1 项目配置
1.1 版本:Version
01.项目结构
a.父pom
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/>
</parent>
<groupId>cn.myslayers</groupId>
<artifactId>libra-product-onboard</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>libra-cloud-common-core</module>
<module>libra-product-onboard-api</module>
<module>libra-product-onboard-entity</module>
<module>libra-product-onboard-service</module>
</modules>
<properties>
<!-- 公共版本配置 -->
<pinyin4j.version>2.5.1</pinyin4j.version>
<swagger2.version>3.0.0</swagger2.version>
<commons-io.version>2.11.0</commons-io.version>
<httpclient.version>4.5.13</httpclient.version>
</properties>
<profiles>
<!-- 方案一:Spring Boot 2.5.x + MyBatis-Plus 3.4.x(最保守稳定) -->
<profile>
<id>stable</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.5.14</spring-boot.version>
<spring-cloud.version>2020.0.4</spring-cloud.version>
</properties>
</profile>
<profiles>
<dependencyManagement>
<dependencies>
<!--======================================= 内部模块版本管理 =======================================-->
<!--======================================= Nacos 系列 ===============================================-->
<!--======================================= Spring 系列 ===============================================-->
<!--======================================= Starter 系列 ==============================================-->
<!--======================================= MyBatis 系列 ==============================================-->
<!--======================================= JSON 序列化系列 ============================================-->
<!--======================================= MapStruct & Lombok =======================================-->
<!--======================================= 工具库系列 ================================================-->
</dependencies>
</dependencyManagement>
<pluginRepositories></pluginRepositories>
<distributionManagement></distributionManagement>
<repositories></repositories>
<build></build>
</project>
b.子pom
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.myslayers</groupId>
<artifactId>libra-product-onboard</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>libra-product-onboard-entity</artifactId>
<dependencies>
<!--======================================= 内部模块版本管理 =======================================-->
<!--======================================= Nacos 系列 ===============================================-->
<!--======================================= Spring 系列 ===============================================-->
<!--======================================= Starter 系列 ==============================================-->
<!--======================================= MyBatis 系列 ==============================================-->
<!--======================================= JSON 序列化系列 ============================================-->
<!--======================================= MapStruct & Lombok =======================================-->
<!--======================================= 工具库系列 ================================================-->
</dependencies>
<build></build>
</project>
02.项目版本
a.方案一:Spring Boot 2.5.x + MyBatis-Plus 3.4.x(最保守稳定)
<java.version>1.8</java.version>
<spring-boot.version>2.5.14</spring-boot.version>
<spring-cloud.version>2020.0.4</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
<spring-cloud-bootstrap.version>3.0.3</spring-cloud-bootstrap.version>
<jackson.version>2.12.7</jackson.version>
<fastjson.version>1.2.83</fastjson.version>
<mybatis.version>3.5.6</mybatis.version>
<mybatis-plus.version>3.4.3.4</mybatis-plus.version>
<mybatis-plus.p6spy>1.8.1</mybatis-plus.p6spy>
<dynamic-datasource.version>3.4.1</dynamic-datasource.version>
<druid.version>1.2.15</druid.version>
<mapstruct.version>1.5.2.Final</mapstruct.version>
<lombok.version>1.18.24</lombok.version>
<knife4j.version>3.0.3</knife4j.version>
<easyexcel.version>3.0.5</easyexcel.version>
<joda-time.version>2.10.14</joda-time.version>
<hutool.version>5.7.22</hutool.version>
<guava.version>31.1-jre</guava.version>
<feign.version>11.10</feign.version>
<oshi.version>6.2.2</oshi.version>
<cxf.version>3.5.5</cxf.version>
b.方案二:Spring Boot 2.7.x + MyBatis-Plus 3.5.x(推荐平衡方案)
<java.version>17</java.version>
<spring-boot.version>2.7.18</spring-boot.version>
<spring-cloud.version>2021.0.9</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
<spring-cloud-bootstrap.version>3.0.3</spring-cloud-bootstrap.version>
<jackson.version>2.13.5</jackson.version>
<fastjson.version>1.2.83</fastjson.version>
<mybatis.version>3.5.10</mybatis.version>
<mybatis-plus.version>3.5.2</mybatis-plus.version>
<mybatis-plus.p6spy>1.8.1</mybatis-plus.p6spy>
<dynamic-datasource.version>3.5.2</dynamic-datasource.version>
<druid.version>1.2.16</druid.version>
<mapstruct.version>1.5.3.Final</mapstruct.version>
<lombok.version>1.18.26</lombok.version>
<knife4j.version>4.1.0</knife4j.version>
<easyexcel.version>3.2.1</easyexcel.version>
<joda-time.version>2.12.2</joda-time.version>
<hutool.version>5.8.15</hutool.version>
<guava.version>31.1-jre</guava.version>
<feign.version>12.1</feign.version>
<oshi.version>6.4.0</oshi.version>
<cxf.version>3.5.5</cxf.version>
<httpclient.version>4.5.14</httpclient.version>
c.方案三:Spring Boot 3.0.x + MyBatis-Plus 3.5.x(现代化方案)
<java.version>17</java.version>
<spring-boot.version>3.0.13</spring-boot.version>
<spring-cloud.version>2022.0.0</spring-cloud.version>
<spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
<spring-cloud-bootstrap.version>3.1.0</spring-cloud-bootstrap.version>
<jackson.version>2.14.2</jackson.version>
<fastjson.version>2.0.25</fastjson.version>
<mybatis.version>3.5.13</mybatis.version>
<mybatis-plus.version>3.5.4</mybatis-plus.version>
<mybatis-plus.p6spy>1.9.1</mybatis-plus.p6spy>
<dynamic-datasource.version>4.1.3</dynamic-datasource.version>
<druid.version>1.2.20</druid.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<lombok.version>1.18.26</lombok.version>
<knife4j.version>4.3.0</knife4j.version>
<easyexcel.version>3.3.2</easyexcel.version>
<joda-time.version>2.12.5</joda-time.version>
<hutool.version>5.8.22</hutool.version>
<guava.version>32.1.2-jre</guava.version>
<feign.version>12.4</feign.version>
<oshi.version>6.4.5</oshi.version>
<cxf.version>4.0.3</cxf.version>
<httpclient.version>4.5.14</httpclient.version>
d.方案四:Spring Boot 3.2.x + MyBatis-Plus 3.5.x(最新稳定)
<java.version>17</java.version>
<spring-boot.version>3.2.5</spring-boot.version>
<spring-cloud.version>2023.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
<spring-cloud-bootstrap.version>3.2.0</spring-cloud-bootstrap.version>
<jackson.version>2.15.2</jackson.version>
<fastjson.version>2.0.33</fastjson.version>
<mybatis.version>3.5.16</mybatis.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<mybatis-plus.p6spy>1.11.0</mybatis-plus.p6spy>
<dynamic-datasource.version>4.2.0</dynamic-datasource.version>
<druid.version>1.2.23</druid.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<lombok.version>1.18.30</lombok.version>
<knife4j.version>4.4.0</knife4j.version>
<easyexcel.version>3.3.4</easyexcel.version>
<joda-time.version>2.12.7</joda-time.version>
<hutool.version>5.8.25</hutool.version>
<guava.version>33.0.0-jre</guava.version>
<feign.version>13.1</feign.version>
<oshi.version>6.4.10</oshi.version>
<cxf.version>4.0.4</cxf.version>
<httpclient.version>4.5.14</httpclient.version>
1.2 依赖:Knife4j
01.依赖
a.坐标
<dependencies>
<!-- 引入Knife4j的官方starter -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies>
b.版本
Spring Boot 2.6.6 您指定的项目基础框架版本
Knife4j 3.0.3 这是与 springfox-boot-starter 兼容的稳定版本,广泛应用于Spring Boot 2.x项目中。它在UI和功能上都非常成熟
02.yml配置
server:
port: 8080 # 自定义端口
# 关键配置!解决Spring Boot 2.6.x以上版本与Swagger的兼容性问题
spring:
# Spring MVC 相关配置
mvc:
pathmatch:
# 设置URL路径匹配策略。ant_path_matcher是Spring的传统策略,支持 `*`, `**`, `?` 等通配符。
matching-strategy: ant_path_matcher
# ===============================================================
# Swagger/Knife4j 配置
# ===============================================================
knife4j:
enable: true # 是否启用 Knife4j
setting:
language: zh-CN # 文档语言
enable-swagger-models: true # 显示模型列表
swagger-model-name: 实体类列表 # 模型分组名称
03.配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2 // 开启Swagger2
// @EnableKnife4j // Knife4j 3.x 版本后,此注解可以省略,但加上也无妨
public class Knife4jConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
// 1. API信息配置
.apiInfo(apiInfo())
// 2. 函数选择器,用于配置哪些接口会生成到文档中
.select()
// 指定扫描的Controller包路径
.apis(RequestHandlerSelectors.basePackage("cn.myslayers.product.onboard.controller"))
// 指定路径处理,any()表示处理所有路径
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("XX项目在线API文档")
.description("本文档详细描述了XX项目的后端API接口。")
.contact(new Contact("你的名字", "http://your.website.com", "[email protected]"))
.version("1.0.0")
.build();
}
}
1.3 依赖:Jackson
01.依赖
a.坐标
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
b.说明
在标准的 Spring Boot Web项目中,我们通常会引入 spring-boot-starter-web。
这个 starter 已经包含了 spring-boot-starter-json,
而后者已经自动引入了 Jackson 所需的核心依赖库 (jackson-databind, jackson-datatype-jsr310 等
02.yml配置
# ===============================================================
# Spring Boot 核心配置 (Spring Core Configuration)
# ===============================================================
spring:
# ===============================================================
# Jackson JSON 序列化/反序列化配置 (Jackson JSON Serialization/Deserialization Configuration)
# ===============================================================
jackson:
# 全局设置日期类型(如 Date, LocalDateTime)序列化为JSON字符串时的格式
date-format: yyyy-MM-dd HH:mm:ss
# 设置时区,保证日期时间处理的一致性,GMT+8是中国标准时间
time-zone: GMT+8
serialization:
# 设置为false,表示将日期序列化为字符串格式(如 "2025-08-20 20:20:20"),而不是时间戳(如 1755778820000)
write-dates-as-timestamps: false
# 设置为false,当遇到空的Java对象时不抛出异常,允许序列化空对象(如只有getter没有属性的对象)
fail-on-empty-beans: false
deserialization:
# 设置为false,表示当JSON字符串中包含Java对象没有的属性时,不抛出异常,而是直接忽略
fail-on-unknown-properties: false
# 支持BigDecimal(已有,正确)- 设置为true时,JSON中的浮点数将被反序列化为BigDecimal而不是Double,提供更高的精度
use-big-decimal-for-floats: true
# 设置为true,允许将单个值作为数组处理。例如:JSON中 "items": "single" 可以反序列化到 List<String> items
accept-single-value-as-array: true
# 设置为true,将空字符串("")视为null对象,防止空字符串导致的反序列化异常
accept-empty-string-as-null-object: true
# 解析器配置,提高容错性
parser:
# 设置为true,允许JSON字符串中包含注释(// 或 /* */),提高配置文件的可读性
allow-comments: true
# 设置为true,允许JSON字符串使用单引号包围字段名和字符串值,而不仅仅是双引号
allow-single-quotes: true
# 设置为true,允许JSON字段名不使用引号包围(如 {name: "value"} 而不是 {"name": "value"})
allow-unquoted-field-names: true
# 全局属性包含策略 - NON_NULL表示序列化时忽略值为null的字段,减少JSON体积
default-property-inclusion: NON_NULL
# 映射器级别配置,用于控制Jackson ObjectMapper的行为
mapper:
# 设置为true,启用大小写不敏感的属性映射。JSON中的 "UserName" 可以映射到 Java 的 "username" 字段
accept-case-insensitive-properties: true
03.配置类
a.总结
依赖:通常无需额外添加,spring-boot-starter-web 已足够。
配置:优先使用 application.yml 进行配置,因为它简洁明了,能满足90%以上的需求。
配置类:仅在需要进行 yml 无法完成的复杂定制(如添加自定义序列化器/反序列化器、注册特定模块等)时,才创建 JacksonConfig 类,并使用 Jackson2ObjectMapperBuilderCustomizer 以追加的方式进行配置。
b.核心思想
Spring Boot 的自动配置机制会读取您在 application.yml 中的所有 spring.jackson.* 配置,并自动应用到一个全局的 ObjectMapper Bean 中。
因此,对于您在 yml 中已经声明的配置,通常不再需要一个Java配置类来重复设置。
c.高级配置
只有当您需要进行一些 yml 文件无法支持的高级定制时,才需要提供一个配置类。
以下是一个推荐的、非侵入式的配置方式,它会在 Spring Boot 自动配置的基础上进行追加配置,而不是完全覆盖它。
d.高级配置-示例:如何为 Java 8 的 LocalDateTime, LocalDate, LocalTime 提供更精细的格式化
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
* Jackson 全局配置
* <p>
* 推荐使用 Jackson2ObjectMapperBuilderCustomizer 来定制由 Spring Boot 自动配置的 ObjectMapper。
* 这样做的好处是不会完全覆盖 Spring Boot 的默认配置,而是在其基础上进行补充和修改。
* 你的 yml 配置依然会生效。
*/
@Configuration
public class JacksonConfig {
// 定义各种日期时间格式
private static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
private static final String DATE_PATTERN = "yyyy-MM-dd";
private static final String TIME_PATTERN = "HH:mm:ss";
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
// 注意:yml中的 spring.jackson.date-format 优先级更高,这里可以作为备用或更精细化的控制。
// 当 yml 中未配置 date-format 时,这里的配置会生效。
// 如果希望代码配置覆盖yml配置,可以采用其他方式(如直接创建ObjectMapper Bean),但通常不推荐。
// 1. 创建 JavaTimeModule 用于处理 Java 8 的日期时间类型
JavaTimeModule javaTimeModule = new JavaTimeModule();
// 2. 添加针对 LocalDateTime 的序列化和反序列化器
javaTimeModule.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATETIME_PATTERN)));
javaTimeModule.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATETIME_PATTERN)));
// 3. 添加针对 LocalDate 的序列化和反序列化器
javaTimeModule.addSerializer(LocalDate.class,
new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_PATTERN)));
javaTimeModule.addDeserializer(LocalDate.class,
new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_PATTERN)));
// 4. 添加针对 LocalTime 的序列化和反序列化器
javaTimeModule.addSerializer(LocalTime.class,
new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_PATTERN)));
javaTimeModule.addDeserializer(LocalTime.class,
new LocalTimeDeserializer(DateTimeFormatter.ofPattern(TIME_PATTERN)));
// 5. 将模块注册到 builder 中
builder.modules(javaTimeModule);
// 示例:如果你想在代码中设置 yml 中的某些属性,可以这样做:
// builder.failOnUnknownProperties(false); // 对应 spring.jackson.deserialization.fail-on-unknown-properties
// builder.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); // 设置命名策略为下划线
};
}
}
1.4 配置:MapStruct
01.错误原因分析
a.根本原因
a.注解处理器执行顺序问题
问题:MapStruct 处理器在 Lombok 处理器之前执行
结果:MapStruct 无法识别 Lombok 生成的 getter/setter 方法
b.依赖缺失
问题:缺少 lombok-mapstruct-binding 依赖
结果:两个框架无法协同工作
c.属性访问权限问题
问题:属性为 private,且 Lombok 未生成访问方法
结果:MapStruct 无法访问属性
b.具体错误分析
a.错误1:Unknown property "id"
问题代码:
@Mapping(target = "id", expression = "java(UUIDUtil.getUUID())")
OnbInterviewEvaluation createEvaluation(String applicantId, String positionId);
-------------------------------------------------------------------------------------------------
错误原因:MapStruct 无法识别 OnbInterviewEvaluation 的 id 属性
b.错误2:No property named "applicantId"
问题代码:
@Mapping(source = "applicantId", target = "applicantId")
PageAllocateDTOs.AuditStatusUpdateDTO createAuditUpdateParams(String applicantId, OnbInterviewAuditStatusEnum auditStatus);
-------------------------------------------------------------------------------------------------
错误原因:MapStruct 无法识别源参数中的 applicantId 属性
02.解决方案
a.解决方案A:修复注解处理器顺序(推荐)
a.步骤1:添加 lombok-mapstruct-binding 依赖
<!-- 在父 POM 的 dependencyManagement 中添加 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
b.步骤2:配置 Maven 编译器插件
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<annotationProcessorPaths>
<!-- 1. 先执行 Lombok -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!-- 2. 再执行 MapStruct -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<!-- 3. 最后执行绑定器 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
b.解决方案B:修复 MapStruct 表达式引用
a.问题:表达式中的类引用不完整
// 错误:使用简单类名
@Mapping(target = "auditStatus", expression = "java(OnbInterviewAuditStatusEnum.getByKey(1))")
// 正确:使用全限定类名
@Mapping(target = "auditStatus", expression = "java(cn.myslayers.product.onboard.api.enums.OnbInterviewAuditStatusEnum.getByKey(1))")
b.修复后的完整映射器
@Mapper
public interface AllocateUpdateMapper {
AllocateUpdateMapper INSTANCE = Mappers.getMapper(AllocateUpdateMapper.class);
@Mapping(target = "id", expression = "java(cn.myslayers.common.core.util.UUIDUtil.getUUID())")
@Mapping(source = "applicantId", target = "applicantId")
@Mapping(source = "applyPosition", target = "applyPosition")
@Mapping(target = "auditStatus", expression = "java(cn.myslayers.product.onboard.api.enums.OnbInterviewAuditStatusEnum.getByKey(1))")
@Mapping(target = "createTime", expression = "java(new java.util.Date())")
@Mapping(target = "createUser", constant = "currentUser")
OnbInterviewPosition createPositionFromDTO(PageAllocateDTOs.AllocateUpdateDTO dto);
}
c.解决方案C:验证实体类属性定义
a.检查 Lombok 注解
// 正确:确保 Lombok 注解正确
@Data
@Accessors(chain = true)
@TableName("onb_interview_evaluation")
public class OnbInterviewEvaluation implements Serializable {
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@TableField("applicant_id")
private String applicantId;
// ... 其他属性
}
b.检查 getter/setter 方法生成
# 编译后检查生成的类
find target/generated-sources -name "*.java" -exec grep -l "getApplicantId\|setApplicantId" {} \;
1.5 配置:DataSource
01.baomidou
a.依赖
<!-- MyBatis-Plus 动态数据源:支持多数据源自动切换 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${dynamic-datasource.version}</version>
</dependency>
b.说明
它的职责是整合和管理多个数据源。它本身并不提供数据库连接池,而是作为一个“管理器”,去集成像 Druid, HikariCP 这样的具体连接池
c.yml配置
spring:
datasource:
# druid 的通用配置需要保留,dynamic-datasource 会读取它
druid:
keepAlive: true
initial-size: 5
# ... 其他 druid 配置 ...
# dynamic 的配置是核心,必须保留
dynamic:
primary: master
strict: true
datasource:
master:
# ... master 配置 ...
onboard:
# ... onboard 配置 ...
02.alibaba
a.依赖
<!-- Druid 数据源:阿里巴巴数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
b.说明
整合单个 Druid 数据源**到 Spring Boot 中。它会自动配置一个 `DataSource` Bean,并让你可以在 `spring.datasource.*` 下进行配置
c.yml配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://...
username: root
password: 123456
1.6 配置:MybatisConfig
01.依赖
a.MyBatis 系列
<!-- MySQL JDBC:MySQL 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus Starter:Spring Boot 集成 MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- Dynamic Datasource:多数据源自动切换(降级到稳定版本) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
</dependency>
<!-- MyBatis-Plus p6spy打印:记录所有的SQL语句及其执行时间 -->
<dependency>
<groupId>com.github.gavlyukovskiy</groupId>
<artifactId>p6spy-spring-boot-starter</artifactId>
</dependency>
<!-- Druid:数据库连接池,用于 DatabaseUtil.java -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
b.JSON 序列化系列
<!-- Jackson Databind:核心 JSON 读写功能 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Jackson Core:底层 JSON 处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<!-- Jackson Annotations:JSON 注解支持 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<!-- FastJSON:阿里巴巴 JSON 序列化工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
c.MapStruct & Lombok
<!-- MapStruct:编译期 Java Bean 转换 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<!-- MapStruct Processor:MapStruct 注解处理器 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
</dependency>
<!-- Lombok:简化 Java Bean 编写 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
d.Maven Compiler:指定 Java 版本与注解处理器
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<!-- 1. 先执行 Lombok -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!-- 2. 再执行 MapStruct -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<!-- 3. 最后执行绑定器 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
02.yml配置
a.Dynamic Datasource:多数据源自动切换
# ===============================================================
# Spring Boot 核心配置 (Spring Core Configuration)
# ===============================================================
spring:
# ===============================================================
# p6spy配置 (p6spy Configuration)
# ===============================================================
p6spy:
# 启用日志记录
enable-logging: true
# 多行模式
multiline: true
# 使用SLF4J日志记录器
logging: slf4j
# 日志格式
log-format: |-
time:%(executionTime)ms || sql:%(sql)
# ===============================================================
# 数据源配置 (Datasource Configuration)
# ===============================================================
datasource:
# ---------------------------------------------------------------
# Druid 连接池通用配置 (Applies to all datasources below)
# ---------------------------------------------------------------
druid:
# 建议配置为true,防止MySQL等数据库8小时无交互自动断开连接的问题
keepAlive: true
# 初始化时建立物理连接的个数
initial-size: 5
# 连接池中保持的最小空闲连接数
min-idle: 5
# 连接池中允许的最大活动连接数
maxActive: 20
# 获取连接时最大等待时间,单位毫秒。如果超过该时间仍未获取到连接,则抛出异常
maxWait: 60000
# 检测并关闭空闲连接的时间间隔,单位毫秒
timeBetweenEvictionRunsMillis: 60000
# 每隔多久将监控数据记录到日志中,单位毫秒。设置为0表示关闭
timeBetweenLogStatsMillis: 60000
# 连接在池中保持空闲而不被驱逐的最长时间,单位毫秒
maxEvictableIdleTimeMillis: 360000
# 连接在池中最小生存的时间,单位毫秒。只有空闲时间超过该值的连接才可能被驱逐
minEvictableIdleTimeMillis: 300000
# 用来检测连接是否有效的SQL查询。DUAL是Oracle和MySQL中的虚拟表
validationQuery: SELECT 'x' FROM DUAL
# 建议配置为true,表示在连接空闲时检测其有效性,不影响性能
testWhileIdle: true
# 获取连接时是否检测其有效性。设置为false可提高性能
testOnBorrow: false
# 归还连接时是否检测其有效性。设置为false可提高性能
testOnReturn: false
# 是否打印被丢弃的连接的日志
logAbandoned: true
# 是否移除长时间未关闭的连接(超时连接)
removeAbandoned: true
# 超时时间,单位秒。当一个连接超过这个时间未使用,则被认为是超时连接并被回收
removeAbandonedTimeout: 1800
# 是否开启 PreparedStatement 缓存(PSCache)
# Oracle下建议开启,MySQL下由于驱动自身有优化,建议关闭
poolPreparedStatements: false
# PreparedStatement 缓存的大小
maxPoolPreparedStatementPerConnectionSize: -1
# 配置Druid的过滤器,用于监控、日志、防火墙等功能。
# stat: 性能统计, log4j2: 日志记录, wall: SQL防火墙
filters: stat,log4j2
# 连接属性配置
# druid.stat.mergeSql=true: 合并相似的SQL,方便统计
# druid.stat.slowSqlMillis=5000: 记录执行时间超过5秒的SQL为慢SQL
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 配置Druid的Web监控过滤器(DruidStatFilter)
web-stat-filter:
# 启用Web监控
enabled: true
# 拦截所有URL请求
url-pattern: "/*"
# 指定不进行统计的静态资源URL
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
# ---------------------------------------------------------------
# 动态数据源配置 (Dynamic Datasource - The REAL configuration)
# ---------------------------------------------------------------
dynamic:
# 设置主数据源(默认数据源)。当代码中未指定使用哪个数据源时,会自动使用名为 'master' 的数据源
primary: master
# 严格模式。设置为true时,如果程序中指定了一个不存在的数据源,会立即报错
strict: true
# 是否启用 p6spy 来监控和分析SQL。p6spy是一个SQL日志工具,可以详细记录SQL执行情况
p6spy: true
# 定义所有的数据源
datasource:
# --- 数据源1 ---
master: # 数据源的逻辑名称
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3307/libra-product-onboard?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
# --- 数据源2 ---
onboard: # 数据源的逻辑名称
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3307/libra-product-onboard?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
b.MyBatis-Plus Starter:Spring Boot 集成 MyBatis-Plus
# ===============================================================
# MyBatis-Plus ORM 框架配置
# ===============================================================
mybatis-plus:
# 指定 Mapper XML 文件的位置。classpath: 表示从类路径下查找
mapper-locations: classpath:/mybatis/*Mapper.xml
# 实体类(Entity)所在的包路径。配置后可在 Mapper XML 中直接使用类名作为别名
type-aliases-package: cn.myslayers.product.onboard.api.entity
# 枚举类所在的包路径。MyBatis-Plus 会自动扫描并处理这些枚举类型与数据库字段的映射
type-enums-package: cn.myslayers.product.onboard.api.enums
# 自定义 TypeHandler 所在包。MyBatis-Plus 启动时会扫描此包下所有继承 BaseTypeHandler,并标注 @MappedJdbcTypes 的类,自动注册到全局配置,用于复杂类型(如 JSON 与对象)映射
type-handlers-package: cn.myslayers.product.onboard.api.hander
# 全局策略配置
global-config:
# ---------- 数据库相关配置 ----------
db-config:
# 主键生成策略:
# AUTO(0): 数据库自增
# INPUT(1): 用户输入
# ASSIGN_ID(2): 雪花算法
# ASSIGN_UUID(3): UUID 字符串
id-type: ASSIGN_UUID
# 字段插入和更新策略:
# IGNORED(0): 忽略判断
# NOT_NULL(1): 只对非 NULL 字段操作
# NOT_EMPTY(2): 只对非空(非 NULL 且非空串/空集合)字段操作
field-strategy: 2
# ❌ 已废弃:3.4.x版本后不再生效,功能已迁移至 configuration.map-underscore-to-camel-case
# 原因:为了与 MyBatis 原生配置保持一致,避免配置重复和混乱
# db-column-underline: true
# ❌ 已废弃:3.4.x版本后功能已内置,无需手动配置
# 原因:MyBatis-Plus 会自动监听 XML 文件变化,该配置项已无实际作用
# refresh-mapper: true
# ❌ 已废弃:3.4.x版本后配置方式发生变化
# 原因:该配置项在新版本中被重新设计,改为更细粒度的控制
# capital-mode: true
# 逻辑删除功能配置:
# 实体类属性名,对应数据库逻辑删除字段
logic-delete-field: deletedFlag
# 逻辑删除时字段值
logic-delete-value: 1
# 未删除时字段值
logic-not-delete-value: 0
# ---------- 自动填充策略(MetaObjectHandler) ----------
# 注册实现了 MetaObjectHandler 接口的 Bean 名称或类,可在插入/更新时自动填充字段
# meta-object-handler: cn.myslayers.product.onboard.config.MyMetaObjectHandler
# MyBatis 原生配置
configuration:
# 开启驼峰命名自动映射(替代已废弃的 db-column-underline)
map-underscore-to-camel-case: true
# 是否开启 MyBatis 二级缓存(默认关闭)
cache-enabled: false
# 查询结果为 NULL 时,是否调用实体 setter(可触发默认值)
call-setters-on-nulls: true
# 指定 MyBatis 使用的日志实现,将 SQL 输出到控制台
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 一级缓存作用域(SESSION 或 STATEMENT)
# local-cache-scope: STATEMENT
# 默认枚举类型处理器(修正为3.4.3.4版本正确路径)
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
03.配置类
a.MybatisPlusConfig
/**
* MyBatis-Plus 统一增强插件配置(3.4.3.4/2.5.14专用版)
* 功能涵盖:分页、乐观锁、多租户、数据安全、自动填充等
*/
@Configuration
public class MybatisPlusConfig {
/**
* MyBatis-Plus 插件主拦截器
* - 分页
* - 乐观锁
* - 多租户
* - 防全表操作
* - 数据变动记录
* - 非法SQL拦截等(如有需要可逐步添加)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(/*@Autowired(required = false) TenantLineHandler tenantLineHandler*/) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// ========== 1. 多租户插件 ==========(如有多租户场景,取消下面注释并自定义TenantLineHandler实现)
/*TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
tenantInterceptor.setTenantLineHandler(tenantLineHandler);
interceptor.addInnerInterceptor(tenantInterceptor);*/
// ========== 2. 动态表名(如有需求) ==========
// interceptor.addInnerInterceptor(new DynamicTableNameInnerInterceptor());
// ========== 3. 分页插件 ==========
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInnerInterceptor.setMaxLimit(1000L); // 单页最大1000,可自定义
interceptor.addInnerInterceptor(paginationInnerInterceptor);
// ========== 4. 乐观锁插件 ==========
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// ========== 5. 阻断全表更新与删除插件 ==========
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
// ========== 6. 数据变动记录插件(如需数据审计等) ==========
// DataChangeRecorderInnerInterceptor dataChangeRecorder = new DataChangeRecorderInnerInterceptor();
// dataChangeRecorder.setBatchUpdateLimit(1000).openBatchUpdateLimitation();
// interceptor.addInnerInterceptor(dataChangeRecorder);
// ========== 7. 非法SQL拦截插件等(如有需求) ==========
// interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
return interceptor;
}
}
b.MyMetaObjectHandler
/**
* MyBatis-Plus 通用字段自动填充处理器【3.4.x标准范式】
*/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
Date now = new Date();
this.strictInsertFill(metaObject, "createTime", Date.class, now);
this.strictInsertFill(metaObject, "updateTime", Date.class, now);
this.strictInsertFill(metaObject, "createUser", String.class, getCurrentUser());
this.strictInsertFill(metaObject, "updateUser", String.class, getCurrentUser());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
this.strictUpdateFill(metaObject, "updateUser", String.class, getCurrentUser());
}
private String getCurrentUser() {
return "system"; // 可集成安全上下文,如 SecurityUtil.getCurrentUserName()
}
}
2 数据管理
2.1 坑点:2类,6种
01.分类1
a.OGNL 表达式相关 Bug
a.字符串比较错误
a.问题描述
在 `<if>` 标签中,OGNL 表达式会将单引号 `'1'` 解析为 `char` 类型,导致与 `String` 类型的变量比较时失败。
b.错误示例
<if test="name != null and name == '1'">
AND id = #{id}
</if>
c.解决方案
a.方案1:使用双引号将字符包装成字符串。
<if test='name != null and name == "1"'>
AND id = #{id}
</if>
b.方案2:调用 `toString()` 方法进行类型转换。
<if test="name != null and name == '1'.toString()">
AND id = #{id}
</if>
b.对于任何非字符串(String)的对象类型,如 Date、Integer、BigDecimal 等,条件判断中应该只使用 != null 来检查,而 != '' 的检查仅对 String 类型有效
a.影响版本
MyBatis 3.3.0
b.问题描述
在此特定版本中,将 `java.util.Date` 类型的参数与空字符串 `''` 进行比较会抛出类型转换异常。
c.错误示例
<if test="createTime != null and createTime != ''">
date(create_time) = date(#{createTime,jdbcType=TIMESTAMP})
</if>
d.解决方案与建议
a.方案1 (推荐): 移除多余的空字符串比较,仅保留 `null` 判断。
<if test="createTime != null">
date(create_time) = date(#{createTime,jdbcType=TIMESTAMP})
</if>
b.方案2 (备选): 避免使用 3.3.0 版本,可降级至 3.2.8 或升级至更高版本。
c.新日期类型(LocalDateTime)映射失效
a.影响版本
MyBatis-Plus 3.1.1+ 配合旧版 Druid
b.问题描述
使用 `LocalDateTime` 等 Java 8 新日期类型时,抛出 `java.sql.SQLFeatureNotSupportedException` 异常。
c.根本原因
MyBatis-Plus 3.1.1 内部依赖的 MyBatis 升级到了 3.5.1,该版本开始要求 JDBC 驱动支持 JDBC 4.2 规范才能正确处理新日期类型。而旧版 Druid (< 1.1.21) 不支持此规范。
d.解决方案
推荐方案:将 Druid 依赖升级至 `1.1.21` 或更高版本。
b.事务管理 Bug
a.saveBatch() 隐式事务问题
a.问题描述
MyBatis-Plus 的 `saveBatch()` 方法内部已经声明了 `@Transactional`。如果外部业务方法也声明了事务,会导致事务嵌套和传播行为异常,可能使外部事务的回滚失效。
b.错误场景示例
@Transactional(rollbackFor = Exception.class)
public Boolean batchInsert(List<Entity> list) {
// 此处 saveBatch 的内部事务可能先于外部事务提交,导致外部异常时无法回滚
entityService.saveBatch(list);
return Boolean.TRUE;
}
c.解决方案
a.方案1:绕过 Service 层,直接调用 Mapper 的批量插入方法,让整个操作处于同一个事务中。
@Transactional(rollbackFor = Exception.class)
public Boolean batchInsert(List<Entity> list) {
return entityMapper.insertBatch(list) > 0;
}
b.方案2:使用手动事务管理,完全控制事务的生命周期。
@Autowired
private MybatisBatch mybatisBatch;
public Boolean batchInsert(List<Entity> list) {
return mybatisBatch.execute(sqlSession -> {
EntityMapper mapper = sqlSession.getMapper(EntityMapper.class);
list.forEach(mapper::insert);
return true;
});
}
b.多数据源下事务管理器冲突
a.问题描述
当应用中配置了多个数据源时,如果存在多个 `PlatformTransactionManager` 类型的 Bean,Spring 将无法确定使用哪一个,导致事务功能失效。
b.解决方案
在使用 `@Transactional` 注解时,通过 `transactionManager` 属性明确指定要使用的事务管理器 Bean 的名称。
@Transactional(transactionManager = "primaryTransactionManager")
public void primaryDbOperation() {
// 主数据库操作
}
@Transactional(transactionManager = "secondaryTransactionManager")
public void secondaryDbOperation() {
// 从数据库操作
}
c.批量更新语法错误
a.错误现象
批量操作提示 `multi-statement not allow`
b.解决方案
连接串添加 `allowMultiQueries=true` 参数
c.参考链接
[MySQL批量操作](https://stackoverflow.com/questions/22829539/mybatis-batch-update-exception)
[连接参数文档](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-security.html)
c.多租户插件 Bug
a.JSQLParser解析失败
a.影响版本
3.4.x - 3.5.x
b.错误现象
`JSQLParserException: Encountered unexpected token`
c.解决方案
方案1:使用 `@InterceptorIgnore(tenantLine="true")` 跳过解析
方案2:升级JSQLParser版本排除冲突依赖
方案3:简化SQL语法避开不支持函数
d.参考链接
[JSQLParser升级指南](https://blog.csdn.net/qq_24615389/article/details/132046825)
[Gitee Issue](https://gitee.com/baomidou/mybatis-plus/issues/I7MJ5G)
b.多语句执行限制
a.影响版本
全版本
b.错误现象
`multi-statement not allow`
c.解决方案
方案1:数据源URL添加 `allowMultiQueries=true`
方案2:Druid配置 `WallConfig.setMultiStatementAllow(true)`
d.参考链接
[Druid配置](https://blog.csdn.net/weixin_41716049/article/details/130953393)
[多语句配置](https://github.com/alibaba/druid/wiki/FAQ)
02.分类2
a.分页插件 Bug
a.分页total为0
a.问题描述
分页查询能够返回数据列表,但分页结果对象中的 `total` (总记录数) 和 `pages` (总页数) 字段恒为 0。
b.常见原因
未在 Spring 配置中注册分页拦截器 Bean。
注册了分页拦截器,但忘记在其配置方法上添加 `@Bean` 注解。
使用了与当前 MyBatis-Plus 版本不兼容的旧版分页拦截器。
c.正确配置示例 (MyBatis-Plus 3.4.x+)
@Configuration
@MapperScan("com.example.dao")
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
b.分页插件不生效
a.影响版本
全版本
b.错误现象
分页参数传入但SQL未添加LIMIT子句
c.解决方案
MyBatis-Plus < 3.4.0: 使用旧的 `PaginationInterceptor`。
MyBatis-Plus >= 3.4.0: 必须使用新的 `MybatisPlusInterceptor` 并添加 `PaginationInnerInterceptor`。
正确配置分页拦截器 `interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL))`
d.参考链接
[配置示例](https://blog.csdn.net/qq_39084776/article/details/124922593)
[调试指南](https://www.cnblogs.com/lcxz8686TL/p/17288561.html)
b.字段策略 Bug
a.updateById更新失效
a.问题描述
调用 MyBatis-Plus 提供的 `updateById()` 方法后,发现数据库中某些字段并未更新,但方法没有报错。
b.根本原因
MyBatis-Plus 的默认字段更新策略是 `NOT_NULL`,这意味着当实体类中某个字段的值为 `null` 时,该字段将不会被包含在最终生成的 UPDATE SQL 语句中。
c.字段策略说明
`IGNORED`: 忽略所有判断,任何字段都会被更新,即使值为 `null`。
`NOT_NULL`: (默认值) 仅更新值不为 `null` 的字段。
`NOT_EMPTY`: 仅更新值不为 `null` 且不为空字符串的字段。
d.解决方案与代码示例
a.方案1:全局修改更新策略。
mybatis-plus:
global-config:
db-config:
field-strategy: IGNORED
b.方案2:在实体类字段上使用注解,进行局部修改。
public class User {
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String nickName; // 允许将 nickName 更新为 null
}
c.方案3:使用 `UpdateWrapper` 精确控制更新行为,此方法会忽略所有策略。
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.eq("id", userId)
.set("nick_name", null); // 强制将 nick_name 设置为 null
userMapper.update(null, wrapper);
b.@TableField注解失效
a.影响版本
全版本
b.错误现象
手写SQL中@TableField映射不生效
c.解决方案
手写SQL场景使用原生MyBatis的resultMap配置
d.参考链接:
[注解失效说明](https://www.cnblogs.com/kuangdaoyizhimei/p/15965206.html)
[ResultMap配置](https://mybatis.org/mybatis-3/sqlmap-xml.html#Result_Maps)
c.Oracle NULL更新异常
a.问题描述
在 Oracle 数据库环境下,当尝试将字段更新为 `null` 时,如果 XML 映射文件中没有明确指定 `jdbcType`,可能会因无法推断类型而抛出异常。
b.解决方案
在 XML 的 `UPDATE` 语句中,为所有可能为 `null` 的字段显式添加 `jdbcType` 属性。
<update id="updateUser">
UPDATE user SET
name = #{name, jdbcType=VARCHAR},
age = #{age, jdbcType=INTEGER}
WHERE id = #{id}
</update>
c.结果映射与类型处理
a.构造器映射依赖字段顺序
a.问题描述
当实体类仅有全参构造器时,MyBatis 会尝试按查询结果的字段顺序与构造器参数顺序进行匹配。一旦 SQL 查询的字段顺序发生改变,就会导致属性映射错乱。
b.解决方案
a.方案1 (推荐): 为实体类提供一个无参构造器。
@NoArgsConstructor // Lombok 注解
public class User { ... }
b.方案2: 在 `<resultMap>` 中使用 `<constructor>` 标签,明确指定结果集列与构造器参数的映射关系。
<resultMap id="userResultMap" type="User">
<constructor>
<idArg column="id" javaType="Long"/>
<arg column="name" javaType="String"/>
</constructor>
</resultMap>
b.Map 结果集处理问题
a.问题一:下划线无法转驼峰
当 `resultType` 设置为 `java.util.Map` 时,MyBatis 默认不会将数据库列的下划线命名(e.g., `user_name`)自动转换为 Map 中 key 的驼峰命名(`userName`)。
b.问题一解决方案
开启全局的 `mapUnderscoreToCamelCase` 配置。
@Configuration
public class MyBatisConfig {
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
configuration.setMapUnderscoreToCamelCase(true);
};
}
}
c.问题二:null 值的 key 丢失
默认情况下,如果查询结果的某个字段值为 `null`,那么在最终返回的 Map 中,将不会包含这个字段对应的 key。
d.问题二解决方案
开启 `call-setters-on-nulls` 配置,强制为 `null` 值的字段也在 Map 中创建 key。
mybatis:
configuration:
call-setters-on-nulls: true
2.2 注解:Annotation
01.MyBaits
a.映射
@Arg: 用于指定构造函数参数的映射
@AutomapConstructor: 自动映射构造函数参数
@ConstructorArgs: 指定构造函数参数的映射
@MapKey: 指定返回的 Map 集合中使用的键
@Result: 指定查询结果的映射
@ResultMap: 引用预定义的结果映射
@Results: 定义一组结果映射
@ResultType: 指定结果类型
@TypeDiscriminator: 用于多态结果映射
b.缓存
@CacheNamespace: 配置命名空间的缓存
@CacheNamespaceRef: 引用另一个命名空间的缓存配置
c.SQL
@Delete: 用于执行删除操作的 SQL 语句
@DeleteProvider: 提供动态删除 SQL 语句
@Insert: 用于执行插入操作的 SQL 语句
@InsertProvider: 提供动态插入 SQL 语句
@Select: 用于执行查询操作的 SQL 语句
@SelectKey: 在插入操作前或后执行的 SQL 语句
@SelectProvider: 提供动态查询 SQL 语句
@Update: 用于执行更新操作的 SQL 语句
@UpdateProvider: 提供动态更新 SQL 语句
d.其他
@Flush: 用于刷新缓存
@Lang: 指定使用的语言驱动
@Many: 指定一对多关系的映射
@Mapper: 标记接口为 MyBatis 映射器
@One: 指定一对一关系的映射
@Options: 配置 SQL 语句的执行选项
@Param: 指定参数名称
@Property: 配置属性
02.MyBatisPlus
a.汇总
@TableName
@Tableld
@TableField
-----------------------------------------------------------------------------------------------------
@Version
@EnumValue
@TableLogic
@SqlParser
@KeySequence
@Interceptorlgnore
@OrderBy
b.@TableName
使用:用于指定实体类对应的数据库表名
示例:
@TableName("student")
public class Student {
// 类属性
}
说明:@TableName 注解用于将实体类与数据库表进行映射,指定表名为 student
c.@TableId
使用:用于指定实体类中的主键字段
示例:
@TableId(value = "id", type = IdType.AUTO)
private Long id;
说明:@TableId 注解指定 id 为主键,并使用自动增长策略
d.@TableField
使用:用于指定实体类字段与数据库表字段的映射关系
示例:
@TableField("name")
private String name;
说明:@TableField 注解将实体类的 name 属性映射到数据库表的 name 字段
e.@Version
使用:用于实现乐观锁
示例:
@Version
private Integer version;
说明:@Version 注解用于标识乐观锁版本字段,更新时自动增加版本号
f.@EnumValue
使用:用于枚举类型字段的映射
示例:
public enum Status {
@EnumValue
ACTIVE(1),
INACTIVE(0);
private final int value;
Status(int value) {
this.value = value;
}
}
说明:@EnumValue 注解用于指定枚举值在数据库中的存储值
g.@TableLogic
使用:用于实现逻辑删除
示例:
@TableLogic
private Integer deleted;
说明:@TableLogic 注解用于标识逻辑删除字段,删除操作时不会物理删除数据,而是更新该字段
h.@SqlParser
使用:用于控制 SQL 解析行为
示例:
@SqlParser(filter = true)
public void customMethod() {
// 自定义方法
}
说明:@SqlParser 注解用于指定方法是否应用 SQL 解析器
i.@KeySequence
使用:用于指定序列主键生成策略
示例:
@KeySequence("seq_user")
public class User {
// 类属性
}
说明:@KeySequence 注解用于指定使用数据库序列生成主键
j.@InterceptorIgnore
使用:用于忽略拦截器
示例:
@InterceptorIgnore(tenantLine = "true")
public void someMethod() {
// 方法体
}
说明:@InterceptorIgnore 注解用于指定方法忽略某些拦截器,如租户拦截器
k.@OrderBy
使用:用于指定排序字段
示例:
@OrderBy("age DESC")
private Integer age;
说明:@OrderBy 注解用于指定查询结果的排序规则
2.3 分页:PageHelper
01.手动分页
a.风格
很清晰地分三步:查数据列表 → 查总数 → 封装 Page;
总是返回 IPage,而不是裸露 List;
用 `Page<>` 来手动 set current、size、total、pages、records;
没有借助 MyBatis-Plus 的自动分页插件,而是手动 SQL + 计算 offset + count。
b.具体操作
controller 层负责接收请求,调用 service 并返回 ApiResult<IPage>。
service 层实现具体分页逻辑:手动查列表和 count,并组装 Page<IPage>。
mapper 层接口包含两个方法:列表查询和总数统计(带参数)。
mapper.xml 件分别写 select 和 count SQL(用 limit/offset 或只查 count)。
02.手动分页-代码
a.controller
@GetMapping("/assigned-to/{assignee}")
@ApiOperation("查询分配给指定人员的应聘者")
public ApiResult<IPage<ApplicantListVO>> getApplicantsByAssignee(
@PathVariable String assignee,
@ApiParam("当前页") @RequestParam(defaultValue = "1") Integer current,
@ApiParam("每页大小") @RequestParam(defaultValue = "10") Integer size) {
try {
IPage<ApplicantListVO> result = applicantService.getApplicantsByAssignee(current, size, assignee);
return ApiResult.success(result);
} catch (Exception e) {
log.error("查询分配给指定人员的应聘者失败", e);
return ApiResult.failure("查询分配给指定人员的应聘者失败: " + e.getMessage());
}
}
b.service
@Transactional(readOnly = true)
public IPage<SomeVO> findSomethingPage(Integer current, Integer size, String condition) {
try {
long offset = (long) (current - 1) * size;
// 1. 查询分页数据
List<SomeEntity> list = someMapper.selectPageByCondition(offset, size, condition);
// 2. 查询总数
Long total = someMapper.countByCondition(condition);
// 3. 转换为 VO
List<SomeVO> voList = new ArrayList<>();
for (SomeEntity entity : list) {
SomeVO vo = new SomeVO();
BeanUtils.copyProperties(entity, vo);
voList.add(vo);
}
// 4. 组装 Page
IPage<SomeVO> result = new Page<>();
result.setCurrent(current);
result.setSize(size);
result.setTotal(total);
result.setPages((long) Math.ceil((double) total / size));
result.setRecords(voList);
return result;
} catch (Exception e) {
log.error("分页查询失败", e);
throw e;
}
}
c.mapper
public interface ApprovalLogMapper {
List<SomeEntity> selectPageByCondition(@Param("offset") Long offset,
@Param("limit") Long limit,
@Param("condition") String condition);
Long countByCondition(@Param("condition") String condition);
}
d.mapper.xml
<select id="selectPageByCondition" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM some_table
WHERE 1=1
<if test="condition != null and condition != ''">
AND some_column = #{condition}
</if>
ORDER BY create_time DESC
LIMIT #{offset}, #{limit}
</select>
<select id="countByCondition" resultType="java.lang.Long">
SELECT COUNT(*)
FROM some_table
WHERE 1=1
<if test="condition != null and condition != ''">
AND some_column = #{condition}
</if>
</select>
2.4 主键:ASSIGN_UUID
01.AutoEntity
a.主键ID不会填充
positionMapper.insertByAll(...) 是自定义 XML 方法(走 MyBatis 原生流程),
不会触发 MyBatis-Plus 的拦截器与 MetaObjectHandler/CustomIdGenerator,因此不会自动生成 ID。
-----------------------------------------------------------------------------------------------------
正确做法(二选一):
在调用前手动设置:position.setId(UUIDUtil.getUUID());,并按需手动补齐公共字段;
或改用 BaseMapper.insert(position),即可自动生成 32 位 ID 且自动填充公共字段。
b.4个公共字段不会填充
只有走 MyBatis-Plus 标准方法(如 BaseMapper.insert/update)时,
@TableField(fill=...) 才会触发 MyMetaObjectHandler,
从而自动填充 createTime/createUser/updateTime/updateUser/deletedFlag。
-----------------------------------------------------------------------------------------------------
走自定义 XML(如 insertByAll)时,是 MyBatis 原生流程,MetaObjectHandler 不会被调用;
这些字段就不会自动填充,除非你在 XML 里显式赋值(例如用 NOW()、传参占位符等)。
c.UUID
通常32位 (去掉横线) 或36位 (带横线)。例如:a8098c1a-f86e-11da-bd1a-00112444be1e
CHAR(32) 或 VARCHAR(32) 或 BINARY(16)
性能较差。因为是随机字符串,MySQL 插入时会导致频繁的页分裂,影响写入性能。
-----------------------------------------------------------------------------------------------------
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
public class Test {
public static void main(String[] args) {
// 1. 生成 32位 不带横线的 UUID (对应 MP 的 ASSIGN_UUID 策略)
// 例如: "b1a2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
String uuid32 = IdWorker.getRandomUUID(32);
// 2. 如果你想生成标准的 36位 带横线的 UUID
// 例如: "b1a2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6"
String uuid36 = IdWorker.getRandomUUID(36);
System.out.println("UUID (32位): " + uuid32);
}
}
d.雪花ID
转换为字符串后最长19位。例如:1760000000201
BIGINT (8 字节)
性能优秀。因为是递增的,类似于自增 ID,插入时索引结构稳定,写入和查询性能高。
-----------------------------------------------------------------------------------------------------
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
public class Test {
public static void main(String[] args) {
// 生成 Long 类型的雪花 ID (数字)
long id = IdWorker.getId();
// 生成 String 类型的雪花 ID (字符串形式,适合转 JSON 给前端)
String idStr = IdWorker.getIdStr();
System.out.println("雪花 ID: " + id);
}
}
02.优先级机制
a.层次结构
1.@TableId 注解指定 (最高优先级)
↓
2.实体类级别配置
↓
3.application.yml 全局配置
↓
4.MyBatis-Plus 框架默认值 (最低优先级)
b.第一优先级:@TableId 注解
a.代码
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
b.说明
作用范围:单个字段
优先级:最高,会覆盖所有其他配置
适用场景:需要特殊处理的主键字段
c.第二优先级:实体类级别配置
a.代码
@TableName(value = "table_name", autoResultMap = true)
public class Entity {
// 实体类级别的配置
}
b.说明
作用范围:整个实体类
优先级:中等,仅次于注解级别
d.第三优先级:application.yml 全局配置
a.代码
mybatis-plus:
# 全局策略配置
global-config:
# ---------- 数据库相关配置 ----------
db-config:
# 主键生成策略:
# AUTO(0): 数据库自增
# INPUT(1): 用户输入
# ASSIGN_ID(2): 雪花算法
# ASSIGN_UUID(3): UUID 字符串
id-type: ASSIGN_UUID
# 字段插入和更新策略:
# IGNORED(0): 忽略判断
# NOT_NULL(1): 只对非 NULL 字段操作
# NOT_EMPTY(2): 只对非空(非 NULL 且非空串/空集合)字段操作
field-strategy: 2
b.说明
作用范围:整个应用
优先级:较低,会被注解覆盖
e.第四优先级:框架默认值
a.MyBatis-Plus 3.3.0+ 之后
默认为 `IdType.ASSIGN_ID`
b.MyBatis-Plus 3.3.0 之前
:默认为 `IdType.NONE`
03.ID生成策略
a.汇总
策略类型 值 说明 适用场景
AUTO 0 数据库自增 单机数据库,性能要求高
NONE 1 无策略 手动指定ID
INPUT 2 用户输入 业务需要指定ID
ASSIGN_ID 3 雪花算法 分布式系统,Long类型
ASSIGN_UUID 4 UUID字符串 分布式系统,String类型
ASSIGN_UUID_32 5 32位UUID 兼容性要求高
b.ASSIGN_UUID
a.代码
// 生成示例:550e8400-e29b-41d4-a716-446655440000
@TableId(type = IdType.ASSIGN_UUID)
private String id;
b.说明
优点:全局唯一、无需协调、支持分布式
缺点:存储空间大、索引性能略低
适用:分布式系统、微服务架构
c.ASSIGN_ID
a.代码
// 生成示例:1234567890123456789
@TableId(type = IdType.ASSIGN_ID)
private Long id;
b.说明
优点:性能高、存储空间小、索引友好
缺点:依赖时钟、时钟回拨问题
适用:高性能要求、单机或小规模集群
04.MapStruct与MyBatis-Plus搭配
a.优先级
a.最高
MapStruct 表达式优先级更高!
b.实体类中的 MyBatis-Plus 注解
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
c.MapStruct 映射器中的表达式
@Mapping(target = "id", expression = "java(cn.myslayers.common.core.util.UUIDUtil.getUUID())")
b.执行顺序
a.步骤1:MapStruct 映射执行
// 当调用这个方法时
position = OnbAllocateMSTs.AllocateUpdateMapper.INSTANCE.createPositionFromDTO(dto);
// MapStruct 会:
// 1. 使用 expression 生成 UUID
// 2. 将生成的 UUID 赋值给 position.id
// 3. 此时 position.id 已经有值了
b.步骤2:MyBatis-Plus 插入执行
// 当执行插入时
positionMapper.insert(position);
// MyBatis-Plus 会:
// 1. 检查 position.id 是否已有值
// 2. 发现 position.id 已经有值(MapStruct 生成的)
// 3. 直接使用这个值,忽略 @TableId 的 IdType.ASSIGN_UUID 策略
c.使用方案
a.方案1(使用):当前配置的实际行为
// 1. MapStruct 生成 UUID
String generatedId = cn.myslayers.common.core.util.UUIDUtil.getUUID();
// 结果:550e8400-e29b-41d4-a716-446655440000
// 2. 赋值给实体
position.setId(generatedId);
// 3. MyBatis-Plus 插入时
// 由于 id 已有值,直接使用,不会重新生成
b.方案2(推荐)
// 让 MyBatis-Plus 处理 ID 生成,MapStruct 只负责业务字段映射
@Mapper
public interface EvaluationMapper {
// 不配置 id 字段,让 MyBatis-Plus 自动处理
@Mapping(source = "applicantId", target = "applicantId")
@Mapping(source = "positionId", target = "applicantPositionId")
// ... 其他业务字段映射
OnbInterviewEvaluation createEvaluation(String applicantId, String positionId);
}
2.5 处理器:JsonArrayTypeHandler
01.SQL数据库
a.核心JSON字段设计
-- 应聘渠道JSON数组字段
`application_channels` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '应聘途径JSON数组字符串'
b.数据库存储示例
// application_channels 字段存储内容
["网站", "微信", "朋友推荐", "社会招聘会"]
02.实体类设计
a.主实体类设计
@Data
@Accessors(chain = true)
@TableName("onb_applicant")
@ApiModel(value = "OnbApplicant对象", description = "应聘者信息表")
public class OnbApplicant implements Serializable {
// JSON数组字段 - 使用专门的数组TypeHandler
@ApiModelProperty("应聘途径(多选)")
@TableField(value = "application_channels", typeHandler = JsonArrayTypeHandler.class)
private ApplicationChannels applicationChannels;
}
b.内部类设计
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonSerialize(using = ApplicationChannelsHandler.ApplicationChannelsSerializer.class)
@JsonDeserialize(using = ApplicationChannelsHandler.ApplicationChannelsDeserializer.class)
public static class ApplicationChannels {
private List<String> channels; // 应聘渠道列表,["网站", "微信", "朋友推荐", "社会招聘会", "校园招聘会"]
/**
* 检查是否为空
* @return true if channels is null or empty
*/
public boolean isEmpty() {
return channels == null || channels.isEmpty();
}
/**
* 获取渠道数量
* @return 渠道数量
*/
public int size() {
return channels == null ? 0 : channels.size();
}
/**
* 检查是否包含指定渠道
* @param channel 渠道名称
* @return true if contains the channel
*/
public boolean contains(String channel) {
return channels != null && channels.contains(channel);
}
}
03.MyBatis-Plus配置层
a.YAML配置
# ===============================================================
# MyBatis-Plus ORM 框架配置
# ===============================================================
mybatis-plus:
# 自定义 TypeHandler 所在包。MyBatis-Plus 启动时会扫描此包下所有继承 BaseTypeHandler,并标注 @MappedJdbcTypes 的类,自动注册到全局配置,用于复杂类型(如 JSON 与对象)映射
type-handlers-package: cn.myslayers.product.onboard.api.hander
b.自定义TypeHandler设计层
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* 通用的JSON数组字符串与特定业务对象转换的MyBatis TypeHandler基类
*
* @param <T> 具体的业务对象类型,如 ApplicationChannels
*/
public abstract class JsonArrayTypeHandler<T> extends BaseTypeHandler<T> {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final TypeReference<List<String>> TYPE_REFERENCE = new TypeReference<List<String>>() {};
/**
* 子类必须实现此方法,用于从业务对象中提取出 List<String>
* @param parameter 业务对象
* @return 提取出的字符串列表
*/
protected abstract List<String> extractList(T parameter);
/**
* 子类必须实现此方法,用于将 List<String> 包装成业务对象
* @param list 字符串列表
* @return 包装后的业务对象
*/
protected abstract T wrapList(List<String> list);
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
try {
List<String> list = extractList(parameter);
if (list == null) {
ps.setString(i, null);
} else {
ps.setString(i, OBJECT_MAPPER.writeValueAsString(list));
}
} catch (JsonProcessingException e) {
throw new SQLException("Failed to serialize object to JSON array", e);
}
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parseJson(rs.getString(columnName));
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parseJson(rs.getString(columnIndex));
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parseJson(cs.getString(columnIndex));
}
private T parseJson(String json) throws SQLException {
if (json == null || json.trim().isEmpty() || "null".equalsIgnoreCase(json.trim())) {
return null;
}
try {
List<String> list = OBJECT_MAPPER.readValue(json, TYPE_REFERENCE);
return wrapList(list);
} catch (Exception e) {
throw new SQLException("Failed to parse JSON array to object", e);
}
}
}
c.具体类型处理器实现
/**
* 针对 ApplicationChannels 类型的具体 TypeHandler
*
* 功能说明:
* 1. 继承 BaseJsonArrayTypeHandler,复用 MyBatis 数据库处理逻辑
* 2. 提供自定义 Jackson 序列化器,确保 JSON 格式为纯数组:["网站", "微信"]
* 3. 而不是对象格式:{"channels": ["网站", "微信"]}
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ApplicationChannelsHandler extends JsonArrayTypeHandler<ApplicationChannels> {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final TypeReference<List<String>> TYPE_REFERENCE = new TypeReference<List<String>>() {};
// ==================== BaseJsonArrayTypeHandler 实现 ====================
/**
* 从 ApplicationChannels 对象中提取 List<String>
* 用于 MyBatis 存储到数据库
*/
@Override
protected List<String> extractList(ApplicationChannels parameter) {
return parameter != null ? parameter.getChannels() : null;
}
/**
* 将 List<String> 包装成 ApplicationChannels 对象
* 用于 MyBatis 从数据库读取
*/
@Override
protected ApplicationChannels wrapList(List<String> list) {
return new ApplicationChannels(list);
}
// ==================== Jackson 序列化器和反序列化器 ====================
/**
* 自定义 Jackson 序列化器
* 将 ApplicationChannels 对象序列化为 JSON 数组格式:["网站", "微信"]
* 而不是对象格式:{"channels": ["网站", "微信"]}
*/
public static class ApplicationChannelsSerializer extends JsonSerializer<ApplicationChannels> {
@Override
public void serialize(ApplicationChannels value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null || value.getChannels() == null) {
gen.writeNull();
} else {
// 直接写数组,不写对象
gen.writeStartArray();
for (String channel : value.getChannels()) {
gen.writeString(channel);
}
gen.writeEndArray();
}
}
}
/**
* 自定义 Jackson 反序列化器
* 将 JSON 数组格式:["网站", "微信"] 反序列化为 ApplicationChannels 对象
*/
public static class ApplicationChannelsDeserializer extends JsonDeserializer<ApplicationChannels> {
@Override
public ApplicationChannels deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 直接读取数组为 List<String>
List<String> channels = p.readValueAs(TYPE_REFERENCE);
return new ApplicationChannels(channels);
}
}
// ==================== 便捷工具方法 ====================
/**
* 将 JSON 数组字符串转换为 ApplicationChannels 对象
* 用于控制器层手动转换
*/
public static ApplicationChannels fromJsonArray(String jsonArray) throws JsonProcessingException {
if (jsonArray == null || jsonArray.trim().isEmpty() || "null".equalsIgnoreCase(jsonArray.trim())) {
return null;
}
List<String> channels = OBJECT_MAPPER.readValue(jsonArray, TYPE_REFERENCE);
return new ApplicationChannels(channels);
}
/**
* 将 ApplicationChannels 对象转换为 JSON 数组字符串
* 用于控制器层手动转换
*/
public static String toJsonArray(ApplicationChannels applicationChannels) throws JsonProcessingException {
if (applicationChannels == null || applicationChannels.getChannels() == null) {
return null;
}
return OBJECT_MAPPER.writeValueAsString(applicationChannels.getChannels());
}
}
04.DTO设计层
a.数据传输对象设计
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public static class ApplicantUpdateDTO implements Serializable {
// DTO中也需要声明TypeHandler,确保参数传递时正确序列化
@TableField(value = "application_channels", typeHandler = ApplicationChannelsHandler.class)
private OnbApplicant.ApplicationChannels applicationChannels;
}
05.Mapper接口与XML配置
a.Mapper接口设计
@Mapper
@DS("onboard")
public interface OnbApplicantMapper {
List<ApplicantListVO> listByDto(@Param("params") ApplicantListDTO dto);
ApplicantDetailVO detailByDto(@Param("params") ApplicantDetailDTO dto);
int insertByDto(@Param("params") ApplicantInsertDTO dto);
int updateByDto(@Param("params") ApplicantUpdateDTO dto);
int deleteByDto(@Param("params") ApplicantDeleteDTO dto);
}
b.XML配置要点
<!-- 更新操作中,MyBatis-Plus会自动调用对应的TypeHandler -->
<update id="updateByDto">
UPDATE onb_applicant
<set>
<if test="params.homeAddress != null">
home_address = #{params.homeAddress},
</if>
<if test="params.pastHistory != null">
past_history = #{params.pastHistory},
</if>
<if test="params.applicationChannels != null">
application_channels = #{params.applicationChannels},
</if>
</set>
WHERE id = #{params.id}
</update>
06.前端数据处理
a.前端接收数据格式
// 从后端接收的数据格式
"applicationChannels": ["网站", "微信", "朋友推荐"]
b.前端表单处理
// Vue.js 组件中的数据处理
data() {
return {
formData: {
applicationChannels: []
}
}
},
methods: {
// 提交数据到后端
submitForm() {
// 前端数据格式与后端DTO完全匹配,无需额外转换
this.$http.post('/api/applicant/update', this.formData)
}
}
07.完整数据流转过程
a.数据写入流程
a.流程
前端表单数据
↓ (HTTP POST)
Controller接收DTO
↓ (参数绑定)
Service业务处理
↓ (调用Mapper)
MyBatis执行SQL
↓ (TypeHandler序列化)
JSON字符串存入数据库
b.说明
1.前端提交:`{homeAddress: {province: "黑龙江省", city: "哈尔滨市"}}`
2.Jackson反序列化:转换为`Address`对象
3.MyBatis参数设置:调用`AddressTypeHandler.setNonNullParameter()`
4.JSON序列化:`{"province":"黑龙江省","city":"哈尔滨市"}`
5.数据库存储:VARCHAR字段存储JSON字符串
b.数据读取流程
a.流程
数据库JSON字符串
↓ (ResultSet获取)
TypeHandler反序列化
↓ (转换为Java对象)
实体类映射
↓ (Service返回VO)
Controller返回前端
↓ (Jackson序列化)
前端接收JSON数据
b.说明
1.数据库读取:`{"province":"黑龙江省","city":"哈尔滨市"}`
2.TypeHandler反序列化:调用`AddressTypeHandler.getNullableResult()`
3.对象转换:创建`Address`对象实例
4.VO映射:Service层转换为前端VO对象
5.JSON响应:Jackson序列化返回给前端
2.6 处理器:JsonObjectTypeHandler
01.SQL数据库
a.核心JSON字段设计
-- 地址相关JSON对象字段
`home_address` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '现家庭住址JSON对象字符串',
`household_register` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '户口所在地JSON对象字符串',
`native_place` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '籍贯JSON对象字符串',
-- 医疗史相关JSON对象字段
`past_history` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '过往史JSON对象字符串',
`current_history` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '现在史JSON对象字符串',
`family_history` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '家族史JSON对象字符串',
`occupational_history` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '职业史JSON对象字符串',
b.数据库存储示例
// home_address 字段存储内容
{
"province": "黑龙江省",
"city": "哈尔滨市",
"district": "道里区",
"detail": "中山东路123号",
"police": "中山派出所",
"village": "中山社区",
"street": "中山东路",
"number": "123号"
}
// past_history 字段存储内容
{
"infectious": true,
"allergy": false,
"surgery": null,
"other": true
}
02.实体类设计
a.主实体类设计
@Data
@Accessors(chain = true)
@TableName("onb_applicant")
@ApiModel(value = "OnbApplicant对象", description = "应聘者信息表")
public class OnbApplicant implements Serializable {
// JSON对象字段 - 使用自定义TypeHandler
@ApiModelProperty("现家庭住址")
@TableField(value = "home_address", typeHandler = AddressTypeHandler.class)
private Address homeAddress;
@ApiModelProperty("户口所在地")
@TableField(value = "household_register", typeHandler = AddressTypeHandler.class)
private Address householdRegister;
@ApiModelProperty("籍贯")
@TableField(value = "native_place", typeHandler = AddressTypeHandler.class)
private Address nativePlace;
@ApiModelProperty("过往史")
@TableField(value = "past_history", typeHandler = PastHistoryTypeHandler.class)
private PastHistory pastHistory;
@ApiModelProperty("现在史")
@TableField(value = "current_history", typeHandler = CurrentHistoryTypeHandler.class)
private CurrentHistory currentHistory;
@ApiModelProperty("家族史")
@TableField(value = "family_history", typeHandler = FamilyHistoryTypeHandler.class)
private FamilyHistory familyHistory;
@ApiModelProperty("职业史")
@TableField(value = "occupational_history", typeHandler = OccupationalHistoryTypeHandler.class)
private OccupationalHistory occupationalHistory;
}
b.内部类设计
@Data
@NoArgsConstructor
public static class Address {
private String province; // 省份
private String city; // 城市
private String district; // 区/县
private String detail; // 详细地址
private String police; // 派出所名称
private String village; // 村或路
private String street; // 街道
private String number; // 门牌号
}
@Data
@NoArgsConstructor
public static class CurrentHistory {
private Boolean disability; // 残疾
private Boolean mental; // 精神疾病
private Boolean colorBlind; // 色盲/色弱
private Boolean other; // 其他
}
@Data
@NoArgsConstructor
public static class FamilyHistory {
private Boolean heart; // 心脏病
private Boolean hypertension; // 高血压
private Boolean diabetes; // 糖尿病
private Boolean other; // 其他
}
@Data
@NoArgsConstructor
public static class OccupationalHistory {
private Boolean ent; // 耳鼻喉职业病
private Boolean organ; // 脏器职业病
private Boolean other; // 其他
}
@Data
@NoArgsConstructor
public static class PastHistory {
private Boolean infectious; // 传染病史
private Boolean allergy; // 过敏史
private Boolean surgery; // 手术史
private Boolean other; // 其他
}
03.MyBatis-Plus配置层
a.YAML配置
# ===============================================================
# MyBatis-Plus ORM 框架配置
# ===============================================================
mybatis-plus:
# 自定义 TypeHandler 所在包。MyBatis-Plus 启动时会扫描此包下所有继承 BaseTypeHandler,并标注 @MappedJdbcTypes 的类,自动注册到全局配置,用于复杂类型(如 JSON 与对象)映射
type-handlers-package: cn.myslayers.product.onboard.api.hander
b.自定义TypeHandler设计层
/**
* 泛型 JSON 对象类型处理器
* 用于处理任意复杂对象与数据库 JSON 字符串之间的转换
*
* @param <T> 需要处理的对象类型,如 Address、PastHistory 等
*/
@MappedJdbcTypes(JdbcType.VARCHAR) // 指定处理的 JDBC 类型为 VARCHAR(JSON 字符串存储)
public class JsonObjectTypeHandler<T> extends BaseTypeHandler<T> {
// Jackson ObjectMapper 实例,用于 JSON 序列化和反序列化
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
// 静态初始化块:配置 ObjectMapper 忽略未知属性,避免反序列化时因字段不匹配而报错
static {
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
// 存储具体的类型信息,用于泛型类型的实际反序列化
private Class<T> type;
/**
* 无参构造函数
* MyBatis 要求 TypeHandler 必须有无参构造函数,用于反射实例化
*/
public JsonObjectTypeHandler() {
// 必需的无参构造函数
}
/**
* 有参构造函数
* 用于具体子类(如 AddressTypeHandler)调用,传入具体的类型信息
*
* @param type 具体的类型 Class 对象,如 Address.class
*/
public JsonObjectTypeHandler(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
}
/**
* 设置非空参数到 PreparedStatement(写入数据库)
* 将 Java 对象序列化为 JSON 字符串存储到数据库
*
* @param ps PreparedStatement 对象
* @param i 参数索引位置
* @param parameter 要序列化的 Java 对象
* @param jdbcType JDBC 类型
* @throws SQLException 如果序列化失败
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
try {
// 使用 Jackson 将 Java 对象转换为 JSON 字符串
ps.setString(i, OBJECT_MAPPER.writeValueAsString(parameter));
} catch (JsonProcessingException e) {
throw new SQLException("Failed to serialize object to JSON", e);
}
}
/**
* 从 ResultSet 中根据列名获取结果(从数据库读取)
*
* @param rs ResultSet 对象
* @param columnName 数据库列名
* @return 反序列化后的 Java 对象
* @throws SQLException 如果反序列化失败
*/
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parseJson(rs.getString(columnName));
}
/**
* 从 ResultSet 中根据列索引获取结果(从数据库读取)
*
* @param rs ResultSet 对象
* @param columnIndex 列索引
* @return 反序列化后的 Java 对象
* @throws SQLException 如果反序列化失败
*/
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parseJson(rs.getString(columnIndex));
}
/**
* 从 CallableStatement 中获取结果(存储过程调用)
*
* @param cs CallableStatement 对象
* @param columnIndex 列索引
* @return 反序列化后的 Java 对象
* @throws SQLException 如果反序列化失败
*/
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parseJson(cs.getString(columnIndex));
}
/**
* 解析 JSON 字符串为 Java 对象的核心方法
*
* @param json 从数据库读取的 JSON 字符串
* @return 反序列化后的 Java 对象,如果 JSON 为空则返回 null
* @throws SQLException 如果解析失败
*/
private T parseJson(String json) throws SQLException {
// 处理空值情况:null、空字符串、"null" 字符串
if (json == null || json.trim().isEmpty() || "null".equalsIgnoreCase(json.trim())) {
return null;
}
try {
// 检查类型是否已初始化(防御性编程)
if (this.type == null) {
// 在某些边缘情况下,如果type未被初始化,抛出异常
// 正常情况下,具体的子类(如AddressTypeHandler)会在构造函数中设置type
throw new SQLException("Type handler was not properly initialized with a target type.");
}
// 使用 Jackson 将 JSON 字符串反序列化为指定类型的 Java 对象
return OBJECT_MAPPER.readValue(json, type);
} catch (Exception e) {
throw new SQLException("Failed to parse JSON to object type: " + (type != null ? type.getName() : "unknown"), e);
}
}
}
c.具体类型处理器实现
/**
* 地址类型处理器:转换的类型为Address
*/
public class AddressTypeHandler extends JsonObjectTypeHandler<Address> {
public AddressTypeHandler() {
super(Address.class);
}
}
/**
* 当前历史类型处理器:转换的类型为CurrentHistory
*/
public class CurrentHistoryTypeHandler extends JsonObjectTypeHandler<CurrentHistory> {
public CurrentHistoryTypeHandler() {
super(CurrentHistory.class);
}
}
/**
* 家庭历史类型处理器:转换的类型为FamilyHistory
*/
public class FamilyHistoryTypeHandler extends JsonObjectTypeHandler<FamilyHistory> {
public FamilyHistoryTypeHandler() {
super(FamilyHistory.class);
}
}
/**
* 职业历史类型处理器:转换的类型为OccupationalHistory
*/
public class OccupationalHistoryTypeHandler extends JsonObjectTypeHandler<OccupationalHistory> {
public OccupationalHistoryTypeHandler() {
super(OccupationalHistory.class);
}
}
/**
* 过去历史类型处理器:转换的类型为PastHistory
*/
public class PastHistoryTypeHandler extends JsonObjectTypeHandler<PastHistory> {
public PastHistoryTypeHandler() {
super(PastHistory.class);
}
}
04.DTO设计层
a.数据传输对象设计
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public static class ApplicantUpdateDTO implements Serializable {
// DTO中也需要声明TypeHandler,确保参数传递时正确序列化
@TableField(value = "home_address", typeHandler = AddressTypeHandler.class)
private OnbApplicant.Address homeAddress;
@TableField(value = "household_register", typeHandler = AddressTypeHandler.class)
private OnbApplicant.Address householdRegister;
@TableField(value = "native_place", typeHandler = AddressTypeHandler.class)
private OnbApplicant.Address nativePlace;
@TableField(value = "past_history", typeHandler = PastHistoryTypeHandler.class)
private OnbApplicant.PastHistory pastHistory;
@TableField(value = "current_history", typeHandler = CurrentHistoryTypeHandler.class)
private OnbApplicant.CurrentHistory currentHistory;
@TableField(value = "family_history", typeHandler = FamilyHistoryTypeHandler.class)
private OnbApplicant.FamilyHistory familyHistory;
@TableField(value = "occupational_history", typeHandler = OccupationalHistoryTypeHandler.class)
private OnbApplicant.OccupationalHistory occupationalHistory;
}
05.Mapper接口与XML配置
a.Mapper接口设计
@Mapper
@DS("onboard")
public interface OnbApplicantMapper {
List<ApplicantListVO> listByDto(@Param("params") ApplicantListDTO dto);
ApplicantDetailVO detailByDto(@Param("params") ApplicantDetailDTO dto);
int insertByDto(@Param("params") ApplicantInsertDTO dto);
int updateByDto(@Param("params") ApplicantUpdateDTO dto);
int deleteByDto(@Param("params") ApplicantDeleteDTO dto);
}
b.XML配置要点
<!-- 更新操作中,MyBatis-Plus会自动调用对应的TypeHandler -->
<update id="updateByDto">
UPDATE onb_applicant
<set>
<if test="params.homeAddress != null">
home_address = #{params.homeAddress},
</if>
<if test="params.pastHistory != null">
past_history = #{params.pastHistory},
</if>
<if test="params.applicationChannels != null">
application_channels = #{params.applicationChannels},
</if>
</set>
WHERE id = #{params.id}
</update>
06.前端数据处理
a.前端接收数据格式
// 从后端接收的数据格式
{
"homeAddress": {
"province": "黑龙江省",
"city": "哈尔滨市",
"district": "道里区",
"detail": "中山东路123号"
},
"pastHistory": {
"infectious": true,
"allergy": false,
"surgery": null,
"other": true
},
}
b.前端表单处理
// Vue.js 组件中的数据处理
data() {
return {
formData: {
homeAddress: {
province: '',
city: '',
district: '',
detail: ''
},
pastHistory: {
infectious: null,
allergy: null,
surgery: null,
other: null
},
}
}
},
methods: {
// 提交数据到后端
submitForm() {
// 前端数据格式与后端DTO完全匹配,无需额外转换
this.$http.post('/api/applicant/update', this.formData)
}
}
07.完整数据流转过程
a.数据写入流程
a.流程
前端表单数据
↓ (HTTP POST)
Controller接收DTO
↓ (参数绑定)
Service业务处理
↓ (调用Mapper)
MyBatis执行SQL
↓ (TypeHandler序列化)
JSON字符串存入数据库
b.说明
1.前端提交:`{homeAddress: {province: "黑龙江省", city: "哈尔滨市"}}`
2.Jackson反序列化:转换为`Address`对象
3.MyBatis参数设置:调用`AddressTypeHandler.setNonNullParameter()`
4.JSON序列化:`{"province":"黑龙江省","city":"哈尔滨市"}`
5.数据库存储:VARCHAR字段存储JSON字符串
b.数据读取流程
a.流程
数据库JSON字符串
↓ (ResultSet获取)
TypeHandler反序列化
↓ (转换为Java对象)
实体类映射
↓ (Service返回VO)
Controller返回前端
↓ (Jackson序列化)
前端接收JSON数据
b.说明
1.数据库读取:`{"province":"黑龙江省","city":"哈尔滨市"}`
2.TypeHandler反序列化:调用`AddressTypeHandler.getNullableResult()`
3.对象转换:创建`Address`对象实例
4.VO映射:Service层转换为前端VO对象
5.JSON响应:Jackson序列化返回给前端
2.7 自动填充:AutoFillInterceptor
00.涉及文件
a.文件
AutoEntity.java
AutoFill.java
AutoFillInterceptor.java
MybatisPlusConfig.java 注册 Interceptor
b.说明
作用于 XML 自定义方法,通过 @AutoInsert / @AutoUpdate / @AutoFill 注解声明填充规则
01.操作说明
a.背景
解决自定义 XML SQL(如 insertByAll/updateByAll)场景下公共字段自动填充难题,让这些方法同样享有与 BaseMapper 一致的体验。
适用:项目中有大量 XML 定制 insert/update SQL,不便每次手工 set id/timestamp/user 字段
期望:"声明注解即可全自动填充,Service 层彻底无公共字段样板代码"
b.实现原理
Mapper 方法上声明 @AutoFill.* 注解,描述需自动填充哪些字段、在哪些 SQL 阶段填充(支持 insert/update/delete 任意组合)
注册拦截器 `AutoFillInterceptor`,在 SQL 执行前解析注解,通过反射为参数 entity 批量填充字段
完全解耦 Service,无需再 set 公共字段,也不用写复杂AOP
Mapper XML 必须保证所有公共字段直接在列清单里,不能再用 if/choose 控制,否则自动填充字段不会出现在 SQL 里
02.配置详解
a.注解体系(AutoFill.java)
枚举(SqlPhase、AuditField、OverwriteMode、IdStrategy)可精细约束
-----------------------------------------------------------------------------------------------------
注解类型:
@Config:完全自定义,最灵活
@AutoInsert:简洁 insert 专用
@AutoUpdate:简洁 update 专用
@Default:类级别默认注解,也可以作为全局兜底策略
-----------------------------------------------------------------------------------------------------
示例:
public interface OnbApplicantWorkMapper {
@AutoFill.AutoInsert
int insertByAll(OnbApplicantWork entity);
@AutoFill.AutoUpdate
int updateByAll(OnbApplicantWork entity);
}
-----------------------------------------------------------------------------------------------------
insertByAll 将自动填充 id、createTime、createUser、updateTime、updateUser、deletedFlag(如同 BaseMapper.insert 效果)
updateByAll 自动填充 updateUser、updateTime(如同 BaseMapper.updateById 效果)
b.关键拦截器配置
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class AutoFillInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 解析注解、批量填充entity字段逻辑
}
}
c.配置注册
@Configuration
public class MybatisPlusConfig {
@Bean
public Interceptor autoFillInterceptor() {
return new AutoFillInterceptor();
}
}
03.使用示例
a.实体类无须改动,继承 AutoEntity 可与 BaseMapper 架构互通
public class OnbApplicantWork extends AutoEntity {
// 业务字段
}
b.Mapper 注解用法(推荐极简)
@Mapper
public interface OnbApplicantWorkMapper {
@AutoFill.AutoInsert
int insertByAll(OnbApplicantWork entity);
@AutoFill.AutoUpdate
int updateByAll(OnbApplicantWork entity);
@AutoFill.Config(phases = {SqlPhase.INSERT}, fields = { AuditField.ID, AuditField.CREATE_TIME, AuditField.CREATE_USER, AuditField.UPDATE_TIME, AuditField.UPDATE_USER, AuditField.DELETED_FLAG }, overwrite = OverwriteMode.IF_NULL, idStrategy = IdStrategy.UUID32_NO_DASH)
int insertByAll(OnbApplicantTrain entity);
@AutoFill.Config(phases = {SqlPhase.UPDATE}, fields = { AuditField.UPDATE_TIME, AuditField.UPDATE_USER }, overwrite = OverwriteMode.ALWAYS)
int updateByAll(OnbApplicantTrain entity);
}
-----------------------------------------------------------------------------------------------------
若有特殊需要可用 @Config 精细声明
c.XML 文件用法重点
保证所有需要自动填充的公共字段(如 id、create_user、create_time)直接在字段清单,不能用逻辑控制包裹!
-----------------------------------------------------------------------------------------------------
<insert id="insertByAll">
INSERT INTO onb_applicant_work
(id, applicant_id, ..., create_time, create_user, update_time, update_user, deleted_flag)
VALUES
(#{id}, #{applicantId}, ..., NOW(), #{createUser}, NOW(), #{updateUser}, 0)
</insert>
d.Service 层一行代码也不用 set 公共字段
autowired OnbApplicantWorkMapper mapper;
public void saveWorked(OnbApplicantWork entity) {
mapper.insertByAll(entity); // 公共字段全自动
}
04.注意事项
a.执行与校验效果
调用自定义 insertByAll/updateByAll,拦截器自动填充所有声明字段。
日志中可观测到“自动填充字段: OnbApplicantWork.id = ...”等关键信息。
Service 永远不用管公共字段赋值
任何字段为空/null/已赋值等可由注解 @Config 控制覆盖策略
b.易错点与避坑提示
XML 一定不要再写 <if test="id != null">id,</if> 这种逻辑,否则 entity 即使被自动填充,字段也不会出现在 SQL 里!要无条件 id,
仅声明 @AutoFill.* 注解的方法有效,纯 XML 不声明注解无效。
若 BaseMapper/自定义注解混用,优先后者,互补无冲突。
拦截器为项目级全局生效,只需注册一次。
c.最佳实践建议
只要是自定义 insert/update XML,均推荐配套声明化 @AutoFill.*(省开发时间,审计可控,代码可维护性极高)
对于极端复杂(动态表名、多类型多状态混合填充),用 @Config 自定义注解灵活配置即可。
00.涉及文件
a.文件
AutoEntity.java
CustomIdGenerator.java
MyMetaObjectHandler.java 使用 @Component 自动注册
b.说明
作用于 BaseMapper.insert() / BaseMapper.update() 等 MyBatis-Plus 内置方法
01.操作说明
a.背景
MyBatis-Plus 对所有继承 `BaseMapper` 的方法(如 insert/updateById)支持自动填充公共字段(如 id、创建/修改时间、创建/修改人、逻辑删除标志)。
如果你的业务SQL不需要复杂自定义,优先用此方案
需要标准增删改查,无自定义SQL。
需全表统一自动填充 id(UUID)、createTime、createUser、updateTime、updateUser、deletedFlag 字段。
b.实现原理
1.实体字段加 `@TableField(... fill = FieldFill.INSERT/UPDATE)`。
2.全局注册 `MetaObjectHandler`,由其统一填充。
3.主键`id`自动生成(如 UUID)需用 `@TableId(type = IdType.ASSIGN_UUID)` + IdentifierGenerator。
4.Service/Controller 不做任何 set,仅填业务字段,其他字段由框架补齐。
02.配置详解
a.实体父类(推荐继承方式,提取所有通用字段)
@Accessors(chain = true)
public class AutoEntity implements Serializable {
// 主键ID(UUID生成,ASSIGN_UUID 策略自动调用CustomIdGenerator)
@TableId(value = "id", type = IdType.ASSIGN_UUID)
protected String id;
// 审计及逻辑删除相关公共字段
@TableField(value = "create_time", fill = FieldFill.INSERT)
protected Date createTime;
@TableField(value = "create_user", fill = FieldFill.INSERT)
protected String createUser;
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
protected Date updateTime;
@TableField(value = "update_user", fill = FieldFill.INSERT_UPDATE)
protected String updateUser;
@TableLogic(value = "0", delval = "1")
@TableField(value = "deleted_flag", fill = FieldFill.INSERT)
protected Integer deletedFlag;
}
-----------------------------------------------------------------------------------------------------
注意:
推荐实体都直接或间接继承 `AutoEntity`,保证字段一致。
每个实体只填自身业务字段,公共的由父类提供。
b.全局填充处理器
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
Date now = new Date();
String currentUser = AuthUtil.getLoginUserNo();
this.strictInsertFill(metaObject, "deletedFlag", Integer.class, 0); // 只在null时填充
this.strictInsertFill(metaObject, "createTime", Date.class, now);
this.strictInsertFill(metaObject, "updateTime", Date.class, now);
this.strictInsertFill(metaObject, "createUser", String.class, currentUser);
this.strictInsertFill(metaObject, "updateUser", String.class, currentUser);
}
@Override
public void updateFill(MetaObject metaObject) {
Date now = new Date();
String currentUser = AuthUtil.getLoginUserNo();
this.strictUpdateFill(metaObject, "updateTime", Date.class, now);
this.strictUpdateFill(metaObject, "updateUser", String.class, currentUser);
}
}
-----------------------------------------------------------------------------------------------------
不会自动填充主键 id,需配合 `@TableId(type = IdType.ASSIGN_UUID)` 并注册自定义 UUID 生成器(见下)
c.统一 UUID 生成器(推荐!),避免 UUID 格式不一致
@Component
@Slf4j
public class CustomIdGenerator implements IdentifierGenerator {
@Override
public String nextUUID(Object entity) {
String uuid = UUIDUtil.getUUID();
return uuid != null ? uuid.replace("-", "") : null; // 返回32位小写无横线UUID
}
}
-----------------------------------------------------------------------------------------------------
推荐所有 UUID 生成统一用 `UUIDUtil.getUUID()`,同一项目、库数据格式100%一致
d.MyBatis-Plus 配置(MybatisPlusConfig)
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
// ...有需要可添加多租户、数据审计等
return interceptor;
}
}
04.使用示例
a.典型实体
public class OnbApplicantPerson extends AutoEntity {
@TableField("applicant_name")
private String applicantName;
// 其他业务字段...
}
b.Mapper
@Mapper
public interface OnbApplicantPersonMapper extends BaseMapper<OnbApplicantPerson> {}
c.Service
@Service
public class OnbApplicantPersonService {
@Autowired
private OnbApplicantPersonMapper personMapper;
public void save(OnbApplicantPerson entity){
// 只set业务数据
personMapper.insert(entity);
// 自动完成id、审计相关的所有填充
}
}
d.典型执行效果
调用 `insert()`,日志会显示自动填充动作及ID生成日志。
数据库内字段完整,前端不用“补”任何审计字段!
05.注意事项
适合绝大多数标准写库操作。
缺点:BaseMapper以外的自定义SQL方法永远不会自动填充。
如果在Mapper里写了自己的 XML,则不能依赖 MetaObjectHandler(详见 AutoFill 文档)。
uuid、时间生成格式高度可控,审计字段填充安全、统一。
不需要关注SQL、无需关心自动值,专注业务即可!
3 后端管理
3.1 实体:3类
00.汇总
a.切面切控制器
切点为所有控制器的所有方法,作用是打日志,判断有无预处理器,有预处理器则执行预处理逻辑,没有则进入控制器。
b.预处理层
对前端数据进行校验和预处理,因为前后分开,前端数据千变万化,所以这一层把数据进行校验和整理。
c.控制器层
调用service层,不做任何多余动作
d.service层
差不多是做事的,但独木难支,单独靠它自己还是需要写些冗余代码,
所以所有的service均继承一个convert接口和IBaseService接口,
IBaseService并包含两个方法,getDomainHelper和getDomainMapper方法,
它俩一个返回的对象用于实体类的创建,封装与校验,后者负责数据库交互,俩对象的类均为模板生成。
e.Mapper层
继承自basemapper,并以符合jpa命名规则的方式包含所有字段的各种单条件查询,负责与数据库交互,提供全方位无死角的crud
f.Helper层
每个实体都有一个helper,负责与实体类打交道
g.生成器
读取实体类,生成java的helper mapper,Typescript代码的实体接口定义,query查询辅助类
h.实体类
需要自行定义
i.过滤器
包含数据权限校验和Token校验
01.常见分类1
a.dto:数据传输对象
FlowApprovalDTOs.java # DTO 聚合模型
OnbApplicantDTOs.java # DTO 聚合模型
PageAllocateDTOs.java # DTO 聚合模型
PageEvaluateDTOs.java # DTO 聚合模型
PageOfferDTOs.java # DTO 聚合模型
b.entity:实体类
OnbApplicant.java # 应聘者实体
OnbApplicantStatus.java # 应聘状态实体
OnbWorkExperience.java # 工作经历实体
OnbTrainingExperience.java # 培训经历实体
OnbFamilyInfo.java # 家庭信息实体
OnbEducationExperience.java # 教育经历实体
c.enums:枚举类
AuditStatusEnum.java # 审核状态枚举
GenderEnum.java # 性别枚举
BloodTypeEnum.java # 血型枚举
EmploymentTypeEnum.java # 用工性质枚举
ProbationDurationEnum.java # 试用期时长枚举
AttachmentTypeEnum.java # 附件类型枚举
InterviewTypeEnum.java # 面试类型枚举
ApprovalStatusEnum.java # 审批状态枚举
SchoolNatureEnum.java # 学历性质枚举
d.mapstruct:映射结构
PageAllocateMSTs.java # MapStruct 聚合模型
PageEvaluateMSTs.java # MapStruct 聚合模型
PageOfferMSTs.java # MapStruct 聚合模型
e.vo:页面对象
FlowApprovalVOs.java # VO 聚合模型
OnbApplicantVOs.java # VO 聚合模型
PageAllocateVOs.java # VO 聚合模型
PageEvaluateVOs.java # VO 聚合模型
PageOfferVOs.java # VO 聚合模型
02.常见分类2
a.dto:数据传输对象
ApplicantDTO.java # 应聘者DTO
ApplicantStatusDTO.java # 应聘状态DTO
InterviewEvaluationDTO.java # 面试评估DTO
EmploymentInfoDTO.java # 录用信息DTO
ApprovalRecordDTO.java # 审批记录DTO
AssignmentDTO.java # 分配DTO
FeedbackDTO.java # 反馈DTO
b.entity:实体类
OnbApplicant.java # 应聘者实体
OnbApplicantStatus.java # 应聘状态实体
OnbWorkExperience.java # 工作经历实体
OnbTrainingExperience.java # 培训经历实体
OnbFamilyInfo.java # 家庭信息实体
OnbEducationExperience.java # 教育经历实体
c.enums:枚举类
AuditStatusEnum.java # 审核状态枚举
GenderEnum.java # 性别枚举
BloodTypeEnum.java # 血型枚举
EmploymentTypeEnum.java # 用工性质枚举
ProbationDurationEnum.java # 试用期时长枚举
AttachmentTypeEnum.java # 附件类型枚举
InterviewTypeEnum.java # 面试类型枚举
ApprovalStatusEnum.java # 审批状态枚举
SchoolNatureEnum.java # 学历性质枚举
d.mapstruct:映射结构
ApplicantMapper.java # 应聘者映射器
InterviewMapper.java # 面试映射器
EmploymentMapper.java # 录用映射器
e.vo:页面对象
ApplicantVO.java # 应聘者展示对象
ApplicantListVO.java # 应聘者列表展示对象
InterviewFeedbackVO.java # 面试反馈展示对象
EmploymentSupplementVO.java # 录用补录展示对象
ApprovalFlowVO.java # 审批流程展示对象
StatisticsVO.java # 统计展示对象
03.常见分类3
a.annotation:自定义注解
DictSn.java # 字典编号注解
ExcelExport.java # Excel导出注解
AuditLog.java # 审计日志注解
b.bo:业务对象
ApplicantAssignmentBo.java # 应聘者分配业务对象
InterviewFeedbackBo.java # 面试反馈业务对象
EmploymentSupplementBo.java # 录用补录业务对象
c.constants:常量类
OnboardConstants.java # 入职管理常量
AuditStatusConstants.java # 审核状态常量
ApprovalConstants.java # 审批常量
d.converter:转换器
ApplicantConverter.java # 应聘者转换器
StatusConverter.java # 状态转换器
InterviewConverter.java # 面试转换器
e.eo:导出对象
ApplicantExportEO.java # 应聘者导出对象
InterviewResultEO.java # 面试结果导出对象
EmploymentStatisticsEO.java # 录用统计导出对象
f.processor:处理器
StatusFlowProcessor.java # 状态流转处理器
ApprovalProcessor.java # 审批处理器
NotificationProcessor.java # 通知处理器
04.聚合模型
a.PageAllocateDTOs
public class PageAllocateDTOs {
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public static class AllocateDetailDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String applicantId;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public static class AllocateUpdateDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String applicantId;
private String applyPosition;
private String assignedDept;
private String assignedUser;
private Date assignedDate;
private String assignedReason;
}
}
b.PageAllocateVOs
public class PageAllocateVOs {
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public static class AllocateDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
private String applicantId;
private String applyPosition;
private String assignedDept;
private String assignedUser;
private Date assignedDate;
private String assignedReason;
}
}
c.PageAllocateMSTs
public class PageAllocateMSTs {
/**
* 分配详情转换器
*/
@Mapper
public interface AllocateDetailMapper {
AllocateDetailMapper INSTANCE = Mappers.getMapper(AllocateDetailMapper.class);
/**
* OnbInterviewPosition 转 AllocateDetailVO
*/
@Mapping(source = "applicantId", target = "applicantId")
@Mapping(source = "applyPosition", target = "applyPosition")
@Mapping(source = "assignedDept", target = "assignedDept")
@Mapping(source = "assignedUser", target = "assignedUser")
@Mapping(source = "assignedDate", target = "assignedDate")
@Mapping(source = "assignedReason", target = "assignedReason")
PageAllocateVOs.AllocateDetailVO positionToDetailVO(OnbInterviewPosition position);
}
}
3.2 日志:log4j
01.使用方式
a.SpringBoot自带的实现 Slf4j + logback
a.日志级别(7级)
TRACE < DEBUG < INFO(默认,只打印INFO后的级别信息) < WARN < ERROR < FATAL < OFF
b.自定义日志级别
logging.level.org.myslayers.HelloWorld=warn
b.自定义:Slf4j + Log4j2
a.原理
应用代码 → SLF4J API → SLF4J 绑定 → Log4j2 实现
b.细节
SLF4J 只是一个接口层,它允许你编写与具体日志实现无关的代码。
SLF4J(Simple Logging Facade for Java)本身并不是一个具体的日志实现,而是一个日志门面。
这意味着你不能“使用 SLF4J 日志”来直接输出日志,而是要通过 SLF4J 提供的 API 来调用底层具体的日志实现
(比如 Logback、Log4j2、java.util.logging 等)来完成日志的输出。
c.简单比喻
Log4j 1.x -> Logback:像是从 Windows XP 升级到了 Windows 7,系统更流畅、界面更美观、功能更强。
Logback -> Log4j 2:像是从 Windows 系统切换到了 macOS,理念和底层架构都有了巨大的变化,旨在提供一种更极致的体验
02.SLF4J绑定架构
a.图示
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 应用代码 │───▶│ SLF4J API │───▶│ SLF4J 绑定 │
│ (@Slf4j/手动) │ │ (slf4j-api) │ │(log4j-slf4j-impl)│
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Log4j2 Core │
│ (log4j-core) │
└─────────────────┘
b.完整的Maven依赖配置
<dependencies>
<!-- Lombok 支持 @Slf4j -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<!-- Log4j2 核心 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
<!-- Log4j2 API -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.20.0</version>
</dependency>
<!-- SLF4J 到 Log4j2 的绑定 - 关键依赖 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.20.0</version>
</dependency>
</dependencies>
c.SLF4J绑定机制详解
1.类路径扫描: SLF4J 在启动时扫描类路径中的 `org/slf4j/impl/StaticLoggerBinder.class`
2.绑定选择: 找到 `log4j-slf4j-impl` 提供的绑定实现
3.Logger 工厂: 使用 `Log4jLoggerFactory` 创建 Logger 实例
4.日志委托: 将所有 SLF4J 调用委托给 Log4j2
d.启动日志示例
SLF4J: Found binding in [jar:file:/path/to/log4j-slf4j-impl-2.20.0.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
03.混合使用,完全可行
a.代码
// 同一个类中混合使用三种方式
@Slf4j // Lombok 生成的 SLF4J Logger
public class MixedLoggingService {
// 手动声明的 SLF4J Logger
private static final Logger manualSlf4jLogger =
LoggerFactory.getLogger("MANUAL_SLF4J");
// 直接使用 Log4j2 Logger
private static final org.apache.logging.log4j.Logger log4j2Logger =
LogManager.getLogger("DIRECT_LOG4J2");
public void demonstrateLogging() {
// 方式1: @Slf4j 注解生成的 logger
log.info("使用 @Slf4j 注解的日志");
// 方式2: 手动声明的 SLF4J logger
manualSlf4jLogger.warn("手动声明的 SLF4J 日志");
// 方式3: 直接使用 Log4j2 logger
log4j2Logger.error("直接使用 Log4j2 的日志");
}
}
b.输出
2025-01-09 14:30:15.123 [main] INFO MixedLoggingService - 使用 @Slf4j 注解的日志
2025-01-09 14:30:15.124 [main] WARN MANUAL_SLF4J - 手动声明的 SLF4J 日志
2025-01-09 14:30:15.125 [main] ERROR DIRECT_LOG4J2 - 直接使用 Log4j2 的日志
3.3 断言:assert
01.两种写法
a.使用断言
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public void cancelOrder(Long orderId) {
// --- 前置条件检查 ---
ServiceAssert.isNotNull(orderId, "订单ID不能为空");
Order order = orderMapper.selectById(orderId);
AssertService.isNotNull(order, "订单不存在");
boolean canCancel = order.getStatus().equals(OrderStatus.PENDING_PAYMENT) ||
order.getStatus().equals(OrderStatus.PENDING_SHIPPING);
// 使用带日志记录的断言,方便排查问题
AssertService.isTrue(canCancel,
"当前订单状态无法取消", // 这个消息给前端
"取消订单失败:订单 " + orderId + " 状态为 " + order.getStatus() // 这个消息记录在后端日志
);
// --- 核心业务逻辑 ---
// 只有所有断言都通过,代码才会执行到这里
System.out.println("订单 " + orderId + " 已成功取消。");
// ... 执行真正的取消订单业务逻辑 ...
}
}
b.不使用断言
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public void cancelOrder(Long orderId) {
// 校验1: 订单ID
if (orderId == null) {
throw new ServiceException("订单ID不能为空");
}
// 查询订单
Order order = orderMapper.selectById(orderId);
// 校验2: 订单是否存在
if (order == null) {
throw new ServiceException("订单不存在");
}
// 校验3: 订单状态
boolean canCancel = order.getStatus().equals(OrderStatus.PENDING_PAYMENT) ||
order.getStatus().equals(OrderStatus.PENDING_SHIPPING);
if (!canCancel) {
throw new ServiceException("当前订单状态无法取消");
}
// ... 执行真正的取消订单业务逻辑 ...
System.out.println("订单 " + orderId + " 已成功取消。");
}
}
02.ServiceAssert类
a.实现
package cn.myslayers.common.core.util;
import com.khict.common.core.exception.ServiceException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Collection;
import java.util.List;
/**
* 业务断言工具类。
* <p>
* 用于在 Service 层进行参数校验或状态检查。当断言失败时,会抛出 {@link ServiceException},
* 中断业务流程。这有助于保持业务方法的代码清洁,并实现快速失败。
*/
public class AssertService {
private static final Logger log = LogManager.getLogger(ServiceAssert.class);
/**
* 断言布尔表达式为 true。
* 如果表达式为 false,则抛出 ServiceException。
*
* @param expression 布尔表达式
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果表达式为 false
*/
public static void isTrue(boolean expression, String message) throws ServiceException {
if (!expression) {
throw new ServiceException(message);
}
}
/**
* 断言对象为 null。
* 如果对象不为 null,则抛出 ServiceException。
*
* @param object 要检查的对象
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果对象不为 null
*/
public static void isNull(Object object, String message) throws ServiceException {
if (object != null) {
throw new ServiceException(message);
}
}
/**
* 断言布尔表达式为 true,并在断言失败时记录一条额外的错误日志。
* 这对于需要记录更详细上下文信息的场景非常有用。
*
* @param expression 布尔表达式
* @param message 如果断言失败,抛出的异常消息(通常给用户看)
* @param logErrorInfo 如果断言失败,记录到错误日志中的详细信息(通常给开发人员看)
* @throws ServiceException 如果表达式为 false
*/
public static void isTrue(boolean expression, String message, String logerrorInfo) throws ServiceException {
if (!expression) {
log.error(logerrorInfo);
throw new ServiceException(message);
}
}
/**
* 断言对象不为 null。
* 这是最常用的断言之一,用于检查方法参数或查询结果。
*
* @param object 要检查的对象
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果对象为 null
*/
public static void isNotNull(Object object, String message) throws ServiceException {
if (object == null) {
throw new ServiceException(message);
}
}
/**
* 断言 List 集合不为空。
*
* @deprecated 此方法已被 {@link #collectionHasValue(Collection, String)} 替代,
* 因为后者更通用(支持所有 Collection 类型)并且能处理 null 的情况。
* @param list 要检查的 List 集合
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果 List 为空
*/
@Deprecated
public static void arrayHasValue(List<? extends Object> list, String message) throws ServiceException {
if (list.isEmpty()) {
throw new ServiceException(message);
}
}
/**
* 断言集合不为 null 且不为空。
*
* @param collection 要检查的集合 (例如 List, Set 等)
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果集合为 null 或为空
*/
public static void collectionHasValue(Collection<? extends Object> collection, String message) throws ServiceException {
if (collection == null || collection.isEmpty()) {
throw new ServiceException(message);
}
}
/**
* 断言字符串有实际内容(非 null、非空字符串、且不只由空白字符组成)。
*
* @param text 要检查的字符串
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果字符串没有实际内容
*/
public static void hasText(String text, String message) throws ServiceException {
if (!hasText(text)) {
throw new ServiceException(message);
}
}
/**
* 断言 List<String> 集合中包含目标字符串。
*
* @param target 目标字符串
* @param resource 字符串 List 集合
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果集合不包含目标字符串
*/
public static void arrayContains(String target, List<String> resource, String message) throws ServiceException {
if (!resource.contains(target)) {
throw new ServiceException(message);
}
}
/**
* 检查字符串是否有文本内容的私有辅助方法。
*
* @param str 要检查的字符串
* @return 如果有文本内容则返回 true,否则返回 false
*/
private static boolean hasText(String str) {
return str != null && !str.isEmpty() && !str.trim().isEmpty();
}
}
3.4 链路:traceId
01.配置说明
a.图示
config/
└── TraceIdConfiguration.java # 统一配置类
├── AopConfig (内部类) # AOP 配置区域
├── ControllerAspect (内部类) # Controller 切面区域
├── MybatisInterceptor (内部类) # MyBatis 拦截器区域
├── 自动配置区域 # WebMvcConfigurer 实现
└── 工具类区域 # MdcTaskDecorator
b.请求链路
HTTP请求 → TraceInterceptor → Controller → Service → Mapper → Database
↓ ↓ ↓ ↓ ↓ ↓
生成traceId 设置MDC 记录日志 记录日志 SQL拦截 执行SQL
↓ ↓ ↓ ↓ ↓ ↓
返回响应 ← 清理MDC ← 返回结果 ← 返回结果 ← 返回结果 ← 查询结果
c.配置类结构
@Configuration
@ConditionalOnProperty(name = "libra.trace.enabled", havingValue = "true", matchIfMissing = true)
public class TraceIdConfiguration implements WebMvcConfigurer {
// ===============================================================
// AOP 配置区域 - 启用 AspectJ 自动代理
// ===============================================================
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
static class AopConfig { }
// ===============================================================
// Controller AOP 切面区域 - 彩色高亮日志监测
// ===============================================================
@Aspect
@Component
@ConditionalOnProperty(name = "libra.trace.controller.aop.enabled", havingValue = "true")
static class ControllerAspect { }
// ===============================================================
// MyBatis 拦截器区域 - 全面数据库操作拦截
// ===============================================================
@Intercepts({...}) // 拦截 Executor + StatementHandler
static class MybatisInterceptor implements Interceptor { }
// ===============================================================
// 自动配置区域 - WebMvcConfigurer + Bean 定义
// ===============================================================
// 拦截器注册、线程池配置、RestTemplate 配置等
}
02.核心组件说明
a.TraceId 生成器
TraceIdGenerator.java 生成唯一的 traceId
格式: `{IP16进制}{时间戳}{递增序号}_{进程ID}`
示例: `ffffffff17257461411001_12345`
b.MDC 工具类
MDCUtils.java 管理 MDC 上下文
功能: 自动生成或从请求头提取 traceId
c.链路追踪上下文
TracerSpanContext.java 管理服务调用链
TracerUtils.java 提供工具方法
TraceIdService.java traceId 服务管理
d.拦截器组件
TraceInterceptor.java HTTP 请求拦截器
TraceClientHttpRequestInterceptor.java HTTP 客户端拦截器
TraceIdMybatisInterceptor.java MyBatis 数据库操作拦截器
TraceControllerAspect.java Controller 层 AOP 监测切面
03.配置说明
a.yml
# ===============================================================
# TraceId 链路追踪系统配置 (TraceId Tracing System Configuration)
# ===============================================================
libra:
trace:
enabled: true # 是否启用 TraceId 链路追踪系统
controller:
aop:
enabled: true # 是否启用 Controller AOP 监测
colorful: true # 是否启用彩色高亮输出
logger:
print:
printLink: true # 是否打印链路信息
b.TraceIdConfiguration.java 侵入式统一配置
@Configuration
@ConditionalOnProperty(name = "libra.trace.enabled", havingValue = "true", matchIfMissing = true)
public class TraceIdConfiguration implements WebMvcConfigurer {
// 统一管理所有 TraceId 相关配置
}
c.自动注册的组件
HTTP 拦截器 (`TraceInterceptor`)
HTTP 客户端拦截器 (`TraceClientHttpRequestInterceptor`)
MyBatis 数据库拦截器 (`MybatisInterceptor`)
Controller AOP 切面 (`ControllerAspect`)
异步线程池 (`traceThreadPoolTaskExecutor`)
RestTemplate (`traceRestTemplate`)
MDC 任务装饰器 (`MdcTaskDecorator`)
3.5 枚举:MapEnum1
01.自定义接口-使用
a.OnbMapEnum
/**
* 审批流程撤回标志枚举
*/
@Getter
@AllArgsConstructor
public enum ApprovalToStartEnum implements OnbMapEnum {
NO(0, "否"),
YES(1, "是");
@EnumValue
private int key;
private String value;
public int getKey() {
return key;
}
public String getValue() {
return value;
}
public static Map<Integer, ApprovalToStartEnum> KEYMAPS = new HashMap<>();
public static Map<String, ApprovalToStartEnum> VALUEMAPS = new HashMap<>();
static {
for (ApprovalToStartEnum enum_ : ApprovalToStartEnum.values()) {
KEYMAPS.put(enum_.getKey(), enum_);
VALUEMAPS.put(enum_.getValue(), enum_);
}
}
public static ApprovalToStartEnum getByKey(Integer key) {
return KEYMAPS.get(key);
}
public static ApprovalToStartEnum getByValue(String value) {
return VALUEMAPS.get(value);
}
}
b.OnbStringKeyMapEnum
/**
* 应聘者状态枚举
*/
@Getter
@AllArgsConstructor
public enum ApplicantStatusEnum {
APPLIED("APPLIED", "已申请"),
INTERVIEW("INTERVIEW", "面试中"),
OFFERED("OFFERED", "已录用"),
REJECTED("REJECTED", "已拒绝");
@EnumValue // <-- MyBatis-Plus: 标记此字段为存入数据库的值
private final String key;
private final String value;
/**
* Jackson 序列化方法
* @JsonValue 告诉 Jackson,当序列化此枚举时,调用此方法,并将其返回值作为 JSON 的值。
* @return 枚举的 key,将作为纯字符串输出
*/
@JsonValue
public String getKey() {
return key;
}
public static final Map<String, ApplicantStatusEnum> KEYMAPS = new HashMap<>();
public static final Map<String, ApplicantStatusEnum> VALUEMAPS = new HashMap<>();
static {
for (ApplicantStatusEnum enum_ : ApplicantStatusEnum.values()) {
KEYMAPS.put(enum_.getKey(), enum_);
VALUEMAPS.put(enum_.getValue(), enum_);
}
}
/**
* 传统的静态查找方法:通过 key 获取枚举实例。
* 同时,此方法也作为 Jackson 的反序列化入口。
* @JsonCreator 告诉 Jackson,当从 JSON 反序列化时,调用此静态方法。
*
* @param key 从 JSON 字符串中读取到的值,或代码中传入的 key
* @return 匹配的枚举实例,或 null
*/
@JsonCreator
public static ApplicantStatusEnum getByKey(String key) {
return KEYMAPS.get(key);
}
/**
* 传统的静态查找方法:通过 value 获取枚举实例。
* @param value 描述文本
* @return 匹配的枚举实例,或 null
*/
public static ApplicantStatusEnum getByValue(String value) {
return VALUEMAPS.get(value);
}
}
02.自定义接口-取值
a.ApprovalToStartEnum (整数Key) 的取值方式
a.直接访问枚举实例
ApprovalToStartEnum isApproved = ApprovalToStartEnum.YES;
System.out.println(isApproved); // 输出: YES
b.从枚举实例中获取属性
ApprovalToStartEnum status = ApprovalToStartEnum.YES;
int key = status.getKey(); // 返回 1
String value = status.getValue(); // 返回 "是"
String name = status.name(); // 返回 "YES"
int ordinal = status.ordinal(); // 返回 1
c.通过 Key 或 Value 反向查找枚举实例
ApprovalToStartEnum statusFromKey = ApprovalToStartEnum.getByKey(1); // 返回 YES
ApprovalToStartEnum statusFromValue = ApprovalToStartEnum.getByValue("否"); // 返回 NO
d.遍历所有枚举实例
for (ApprovalToStartEnum status : ApprovalToStartEnum.values()) {
System.out.println("Key: " + status.getKey() + ", Value: " + status.getValue());
}
b.ApplicantStatusEnum (字符串Key) 的取值方式
a.直接访问枚举实例
ApplicantStatusEnum currentStatus = ApplicantStatusEnum.INTERVIEW;
System.out.println(currentStatus); // 输出: INTERVIEW
b.从枚举实例中获取属性
ApplicantStatusEnum status = ApplicantStatusEnum.OFFERED;
String key = status.getKey(); // 返回 "OFFERED"
String value = status.getValue(); // 返回 "已录用"
String name = status.name(); // 返回 "OFFERED"
int ordinal = status.ordinal(); // 返回 2
c.通过 Key 或 Value 反向查找枚举实例
ApplicantStatusEnum statusFromKey = ApplicantStatusEnum.getByKey("APPLIED"); // 返回 APPLIED
ApplicantStatusEnum statusFromValue = ApplicantStatusEnum.getByValue("已拒绝"); // 返回 REJECTED
d.遍历所有枚举实例
for (ApplicantStatusEnum status : ApplicantStatusEnum.values()) {
System.out.println("Key: " + status.getKey() + ", Value: " + status.getValue());
}
03.自定义接口-实例比较
a.使用 == 运算符 (最佳实践)
a.代码
ApprovalToStartEnum currentStatus = ApprovalToStartEnum.YES;
if (currentStatus == ApprovalToStartEnum.YES) {
System.out.println("审批已通过!");
}
b.优点
性能最高:直接进行内存地址比较。
编译期类型安全:不同类型枚举无法比较。
Null安全:`null == ApprovalToStartEnum.YES` 返回 `false`。
b.使用 .equals() 方法
a.代码
ApprovalToStartEnum currentStatus = ApprovalToStartEnum.YES;
if (currentStatus.equals(ApprovalToStartEnum.YES)) {
System.out.println("审批已通过!");
}
b.缺点
存在 `NullPointerException` 风险。
性能稍差:方法调用有微小开销。
失去编译期类型安全。
c.使用 switch 语句 (推荐)
ApplicantStatusEnum currentStatus = ApplicantStatusEnum.OFFERED;
switch (currentStatus) {
case APPLIED:
System.out.println("处理“已申请”状态的逻辑...");
break;
case INTERVIEW:
System.out.println("处理“面试中”状态的逻辑...");
break;
case OFFERED:
System.out.println("处理“已录用”状态的逻辑...");
break;
case REJECTED:
System.out.println("处理“已拒绝”状态的逻辑...");
break;
default:
System.out.println("未知状态");
break;
}
d.与枚举的属性进行比较
int statusCodeFromApi = 1;
String statusKeyFromApi = "APPLIED";
if (statusCodeFromApi == ApprovalToStartEnum.YES.getKey()) {
System.out.println("API状态码 1 代表“是”。");
}
if (statusKeyFromApi.equals(ApplicantStatusEnum.APPLIED.getKey())) {
System.out.println("API状态Key 'APPLIED' 代表“已申请”。");
}
04.自定义接口-MapEnum
a.注解
/**
* MapEnum 接口:约定枚举必须提供一个数值型键(getKey)和一个文本型值(getValue)。
* 通过 JsonSerialize 和 JsonDeserialize 注解,绑定自定义的序列化/反序列化逻辑。
*
* @param <I> 枚举键的类型,继承自 Number,通常为 Integer 或 Long。
*/
@JsonSerialize(using = MapEnumSerializer.class) // 序列化:枚举 -> JSON 对象
@JsonDeserialize(using = MapEnumDeserializer.class) // 反序列化:JSON -> 枚举
public interface MapEnum<I extends Number> {
/**
* 获取枚举的数字键,用于数据库存储及 JSON 传输中的 key 字段。
* 示例:1、2、3 等。
*
* @return 枚举对应的数字键
*/
int getKey();
/**
* 获取枚举的显示文本,用于前端展示或日志记录。
* 示例:"初中"、"大专" 等。
*
* @return 枚举对应的文本值
*/
String getValue();
}
b.序列化
/**
* MapEnumSerializer 实现:
* 1. 将入参对象强制转换为 MapEnum,确保可以调用 getKey() 和 getValue()。
* 2. 创建一个 Map,填充 "key"(数字键)、"value"(文本值)、"logicType"(固定值 "enums")。
* 3. 使用 JsonGenerator.writeObject 将整个 Map 写入 JSON 输出。
*
* 序列化示例输出:
* {
* "key": 4,
* "value": "大专",
* "logicType": "enums"
* }
*/
public class MapEnumSerializer extends JsonSerializer {
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
@SuppressWarnings("unchecked")
MapEnum<Number> mapEnum = (MapEnum<Number>) value;
Map<String, Object> out = new HashMap<>(3);
out.put("key", mapEnum.getKey());
out.put("value", mapEnum.getValue());
out.put("logicType", "enums");
gen.writeObject(out);
}
}
c.反序列化
/**
* MapEnumDeserializer 实现:
* 1. 读取当前 JSON 属性的节点:可能是 ObjectNode、IntNode、TextNode 或 ArrayNode。
* 2. 尝试从节点中提取名为 "key" 的值,并转换成 Integer。
* 3. 通过 BeanUtils.findPropertyType 动态获取当前属性对应的枚举类类型。
* 4. 遍历该枚举类的所有常量,匹配 getKey() 与提取的 key 值,返回对应枚举实例。
* 5. 如果未匹配,则返回 null(可根据业务需求改为抛异常或返回默认枚举)。
*/
public class MapEnumDeserializer extends JsonDeserializer<MapEnum<Number>> {
/**
* 构造方法:可用于初始化日志或调试信息。
*/
protected MapEnumDeserializer() {
// 仅示例打印,实际可移除或改为日志输出
System.out.println("初始化 MapEnumDeserializer");
}
@Override
public MapEnum<Number> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 1. 将整个 JSON 内容读取为通用树节点
TreeNode treeNode = p.getCodec().readTree(p);
Integer key = null;
// 2. 根据节点类型,提取 key 值
if (treeNode instanceof ObjectNode) {
// 对象格式:{ "key": 4, "value": "大专", "logicType": "enums" }
ObjectNode obj = (ObjectNode) treeNode;
// 尝试以整数形式读取
key = obj.get("key").asInt();
if (key == null) {
// 当 asInt() 返回 null 时,再尝试以文本形式读取并转换
key = Integer.valueOf(obj.get("key").asText());
}
} else if (treeNode instanceof IntNode) {
// 纯数字格式:4
key = ((IntNode) treeNode).asInt();
} else if (treeNode instanceof TextNode) {
// 文本数字格式:"4"
key = Integer.valueOf(((TextNode) treeNode).asText());
} else if (treeNode instanceof ArrayNode) {
// 数组格式:[ { "key": 4 } ]
ArrayNode array = (ArrayNode) treeNode;
key = array.findValue("key").asInt();
}
// 3. 获取当前 JSON 属性名及其所属对象,用于反射获取字段类型
String propName = p.getCurrentName();
Class<?> parentClass = ctxt.getParser().getCurrentValue().getClass();
@SuppressWarnings("unchecked")
// 4. 通过 BeanUtils 查找父对象中名为 propName 的字段类型,转换为 MapEnum 子类型
Class<? extends MapEnum<Number>> enumClass =
(Class<? extends MapEnum<Number>>) BeanUtils.findPropertyType(propName, new Class<?>[]{parentClass});
// 5. 遍历枚举常量,匹配 key
for (MapEnum<Number> constant : enumClass.getEnumConstants()) {
if (constant.getKey() == key) {
return constant;
}
}
// 6. 未找到匹配时返回 null(可替换逻辑)
return null;
}
}
05.自定义接口-StringKeyMapEnum
a.注解
/**
* StringKeyMapEnum 接口:约定枚举必须提供一个字符串键(getKey)和一个文本值(getValue)。
*/
@JsonSerialize(using = StringKeyMapEnumSerializer.class) // 序列化:枚举 -> 字符串 key
@JsonDeserialize(using = StringKeyMapEnumDeserializer.class) // 反序列化:字符串/对象 -> 枚举
public interface StringKeyMapEnum {
/**
* 获取枚举在 JSON 中的字符串键。
* @return 枚举对应的字符串键
*/
String getKey();
/**
* 获取枚举的文本展示值。
* @return 枚举对应的显示文本
*/
String getValue();
}
b.序列化
/**
* StringKeyMapEnumSerializer 实现:
* 1. 强制转换枚举为 StringKeyMapEnum。
* 2. 调用 getKey() 输出字符串。
*
* 序列化示例输出:
* "MY_ENUM_KEY"
*/
public class StringKeyMapEnumSerializer extends JsonSerializer {
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
StringKeyMapEnum mapEnum = (StringKeyMapEnum) value;
gen.writeObject(mapEnum.getKey());
}
}
c.反序列化
/**
* StringKeyMapEnumDeserializer 实现:
* 1. 读取 JSON 为 TreeNode。
* 2. 若为 ObjectNode,则获取 "key" 属性文本;TextNode 则直接获取文本;ArrayNode 则查找子节点 "key"。
* 3. 获取当前属性名与父对象类型,通过 BeanUtils 查找字段实际类型(枚举类)。
* 4. 遍历枚举常量,采用 String.equals 比对 key,并返回匹配常量。
* 5. 未匹配时返回 null(或抛异常/默认值)。
*/
public class StringKeyMapEnumDeserializer extends JsonDeserializer<StringKeyMapEnum> {
protected StringKeyMapEnumDeserializer() {
// 空构造,框架反射调用
}
@Override
@SuppressWarnings("unchecked")
public StringKeyMapEnum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode treeNode = p.getCodec().readTree(p);
String key = null;
if (treeNode instanceof ObjectNode) {
key = ((ObjectNode) treeNode).get("key").asText();
} else if (treeNode instanceof TextNode) {
key = ((TextNode) treeNode).asText();
} else if (treeNode instanceof ArrayNode) {
key = ((ArrayNode) treeNode).findValue("key").asText();
}
String propName = p.getCurrentName();
Class<?> parent = ctxt.getParser().getCurrentValue().getClass();
Class<? extends StringKeyMapEnum> enumClass =
(Class<? extends StringKeyMapEnum>) BeanUtils.findPropertyType(propName, new Class<?>[]{parent});
for (StringKeyMapEnum constant : enumClass.getEnumConstants()) {
if (constant.getKey().equals(key)) {
return constant;
}
}
return null;
}
}
3.6 枚举:MapEnum2
01.MP接口-使用
a.MapEnum
/**
* 审批流程撤回标志枚举
*
* 1. 移除 implements MapEnum
* 2. @EnumValue: 标记 key 为数据库存储值
* 3. @JsonFormat: 标记序列化为 JSON 对象
* 4. @JsonCreator: 标记反序列化的工厂方法
*/
@Getter
@AllArgsConstructor
@JsonFormat(shape = JsonFormat.Shape.OBJECT) // <-- Jackson: 序列化为JSON对象
public enum ApprovalToStartEnum {
NO(0, "否"),
YES(1, "是");
@EnumValue // <-- MyBatis-Plus: 标记此字段为存入数据库的值
private final int key;
// @JsonProperty 注解确保在 @JsonFormat(shape=OBJECT) 模式下,此字段能被正确序列化
@JsonProperty("value")
private final String value;
// @JsonProperty 同样可用于 getter 方法
@JsonProperty("key")
public int getKey() {
return key;
}
/**
* Jackson 反序列化工厂方法
* @JsonCreator 告诉 Jackson 使用此方法从 JSON 创建枚举实例。
* 支持多种入参格式:
* 1. 纯数字: 1
* 2. 字符串: "1"
* 3. 对象: { "key": 1, "value": "是" } (只会用到 key)
* @param key 从 JSON 中提取的 key 值
* @return 匹配的枚举实例,或 null
*/
@JsonCreator
public static ApprovalToStartEnum getByKey(Object key) {
if (key == null) {
return null;
}
// 兼容不同数字类型
int intKey;
if (key instanceof Number) {
intKey = ((Number) key).intValue();
} else {
try {
intKey = Integer.parseInt(key.toString());
} catch (NumberFormatException e) {
return null; // 或者抛出异常
}
}
return Stream.of(ApprovalToStartEnum.values())
.filter(e -> e.getKey() == intKey)
.findFirst()
.orElse(null);
}
}
b.StringKeyMapEnum
/**
* 流程节点枚举
*
* 1. @EnumValue: 标记 key (字符串) 为数据库存储值
* 2. @JsonValue: 标记 getKey() 的返回值为 JSON 序列化的值
* 3. @JsonCreator: 标记反序列化的工厂方法
*/
@Getter
@AllArgsConstructor
public enum ProcessNodeEnum {
APPLY_SUBMIT("APPLY_SUBMIT", "发起申请"),
DEPT_APPROVE("DEPT_APPROVE", "部门审批"),
HR_APPROVE("HR_APPROVE", "人事审批"),
FINAL_CONFIRM("FINAL_CONFIRM", "最终确认");
@EnumValue // <-- MyBatis-Plus: 标记此字段为存入数据库的值
private final String key;
private final String value;
/**
* Jackson 序列化方法
* @JsonValue 告诉 Jackson,当序列化此枚举时,调用此方法,并将其返回值作为 JSON 的值。
* @return 枚举的 key,将作为纯字符串输出
*/
@JsonValue
public String getKey() {
return key;
}
/**
* Jackson 反序列化工厂方法
* @param key 从 JSON 字符串中读取到的值
* @return 匹配的枚举实例,或 null
*/
@JsonCreator
public static ProcessNodeEnum getByKey(String key) {
return Stream.of(ProcessNodeEnum.values())
.filter(e -> e.getKey().equals(key))
.findFirst()
.orElse(null);
}
}
02.MP接口-取值
a.ApprovalToStartEnum (整数Key,序列化为JSON对象)
a.直接访问枚举实例
ApprovalToStartEnum yesEnum = ApprovalToStartEnum.YES;
ApprovalToStartEnum noEnum = ApprovalToStartEnum.NO;
b.从枚举实例中获取属性
ApprovalToStartEnum currentApproval = ApprovalToStartEnum.YES;
int key = currentApproval.getKey(); // --> 1
String value = currentApproval.getValue(); // --> "是"
String name = currentApproval.name(); // --> "YES"
int ordinal = currentApproval.ordinal(); // --> 1
c.通过 Key 反向查找枚举实例
ApprovalToStartEnum fromInt = ApprovalToStartEnum.getByKey(1); // --> YES
ApprovalToStartEnum fromString = ApprovalToStartEnum.getByKey("0"); // --> NO
ApprovalToStartEnum fromNull = ApprovalToStartEnum.getByKey(null); // --> null
d.遍历所有枚举实例
for (ApprovalToStartEnum status : ApprovalToStartEnum.values()) {
System.out.println(String.format("常量名: %s, Key: %d, Value: %s",
status.name(), status.getKey(), status.getValue()));
}
b.ProcessNodeEnum (字符串Key,序列化为JSON字符串)
a.直接访问枚举实例
ProcessNodeEnum node = ProcessNodeEnum.DEPT_APPROVE;
b.从枚举实例中获取属性
ProcessNodeEnum currentNode = ProcessNodeEnum.APPLY_SUBMIT;
String key = currentNode.getKey(); // --> "APPLY_SUBMIT"
String value = currentNode.getValue(); // --> "发起申请"
String name = currentNode.name(); // --> "APPLY_SUBMIT"
int ordinal = currentNode.ordinal(); // --> 0
c.通过 Key 反向查找枚举实例
ProcessNodeEnum fromKey = ProcessNodeEnum.getByKey("HR_APPROVE"); // --> HR_APPROVE
ProcessNodeEnum fromInvalidKey = ProcessNodeEnum.getByKey("INVALID_KEY"); // --> null
d.遍历所有枚举实例
for (ProcessNodeEnum node : ProcessNodeEnum.values()) {
System.out.println(String.format("Key: %s, Value: %s", node.getKey(), node.getValue()));
}
03.MP接口-实例比较
a.使用 == 运算符 (最佳实践)
a.代码
ProcessNodeEnum currentNode = ProcessNodeEnum.DEPT_APPROVE;
if (currentNode == ProcessNodeEnum.DEPT_APPROVE) {
System.out.println("当前节点是部门审批。");
}
b.优点
性能最佳:直接进行内存地址比较。
编译期类型安全:不同类型枚举无法比较。
Null安全:`null == ProcessNodeEnum.DEPT_APPROVE` 返回 `false`。
b.使用 .equals() 方法
a.代码
ProcessNodeEnum currentNode = ProcessNodeEnum.DEPT_APPROVE;
if (currentNode.equals(ProcessNodeEnum.DEPT_APPROVE)) {
System.out.println("当前节点是部门审批。");
}
b.缺点
存在 `NullPointerException` 风险。
性能稍差:方法调用有微小开销。
失去部分编译期类型安全。
c.使用 switch 语句 (推荐)
ProcessNodeEnum currentNode = ProcessNodeEnum.HR_APPROVE;
switch (currentNode) {
case APPLY_SUBMIT:
System.out.println("执行“发起申请”的后续操作...");
break;
case DEPT_APPROVE:
System.out.println("执行“部门审批”的后续操作...");
break;
case HR_APPROVE:
System.out.println("执行“人事审批”的后续操作...");
break;
case FINAL_CONFIRM:
System.out.println("流程结束!");
break;
default:
throw new IllegalStateException("未知的流程节点: " + currentNode);
}
d.与枚举的属性进行比较
int statusFromDB = 1;
String nodeKeyFromAPI = "APPLY_SUBMIT";
if (statusFromDB == ApprovalToStartEnum.YES.getKey()) {
System.out.println("数据库中的状态 1 代表“是”。");
}
if (nodeKeyFromAPI.equals(ProcessNodeEnum.APPLY_SUBMIT.getKey())) {
System.out.println("API传入的Key 'APPLY_SUBMIT' 代表“发起申请”。");
}
3.7 枚举:OnbMapEnum
01.汇总
a.区别
a.序列化:枚举 -> 变成JSON对象,放进去
OnbMapEnum 变成 {"key": 4, "value": "大专", "logicType": "enums"}
OnbStringKeyMapEnum 变成 {"key": "01", "value": "汉族", "logicType": "enums"}
b.反序列化:JSON -> 变成枚举,取出来
"key": 4 变成 OnbMapEnum
"key": "01" 变成 OnbStringKeyMapEnum
c.自定义处理器:2部分内容,放进去+取出来
1.设置非空参数时,将枚举的 key 作为整数写入 PreparedStatement。
2.从 JDBC 查询结果集 ResultSet 或 CallableStatement 中读取整数类型 key,并通过枚举常量中的 key 进行匹配返回对应枚举实例。
3.未匹配到对应枚举时返回 null
b.方案选型
a.方案A(推荐,稳妥):保留 Java 枚举常量集,DB 管理 value/排序/启用
优点:保留编译期类型安全;OnbStringKeyMapEnum 与现有 TypeHandler/Jackson 全兼容;getByKey 不变
限制:key 集合固定,不能在运行时新增新的 key
b.方案B(纯字典化):完全不用 Java enum(字段用 String 存 key;下拉清单/展示走“字典服务/缓存”)
优点:DB 可完全控制可选项的增删改
影响:实体/DTO/序列化/校验/持久化/前端/报表需联动改造,失去编译期类型安全
c.方案C(折中):保留少量强类型常量(如 OTHER),其余扩展走字典
优点:兼顾强类型分支与少量动态扩展
影响:需要清晰边界与治理
d.总结
若需要编译期类型安全与现有 TypeHandler/Jackson 行为,保留 Java 枚举更稳妥;
数据库仅管理显示文案(value)、排序、启用状态等。若需运行时新增/删减选项,应改为“纯字典化”方案(不再使用 Java enum)。
c.OnbApplicantNationEnum 的两种形态与用法
a.形态一(保留 enum,DB 覆盖 value)
代码保持当前结构与常量列表不变(key 集合固定)
启动时从 enum_config 读出 key -> value,通过反射覆盖各常量的 value 字段,并重建 VALUEMAPS
用法不变:OnbApplicantNationEnum.getByKey("01").getValue()(value 将是 DB 中维护的文本)
b.形态二(不再使用 enum,纯字典化)
删除枚举类,新增“字典访问器/服务”(如 DictService),统一提供 list(enumType)、get(enumType,key)、refresh(enumType)
实体字段改为 String key;序列化输出 {key,value} 或仅 key,由服务回填 value
MyBatis 不再需要 Enum TypeHandler;校验改为基于字典缓存的运行时校验
02.示例代码
a.ProcessNodeEnum枚举
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
/**
* -----------------------------------------------------------------------------------
* ProcessNodeEnum:流程节点枚举
* 说明:
* 1. 用于描述业务流转的多个节点,每个节点包含唯一 key、对应中文描述 value 以及 logicType。
* 2. 通过 @EnumValue 注解,将 key 作为 MyBatis-Plus 存储到数据库的值。
* 3. 通过 @JsonValue/@JsonCreator 支持 Jackson 的自定义序列化与反序列化为完整 JSON 对象。
* 4. 增加 logicType 字段,方便前端/后端业务统一识别为枚举类型。
* -----------------------------------------------------------------------------------
*/
@Getter
@AllArgsConstructor
public enum ProcessNodeEnum {
// 发起申请节点
APPLY_SUBMIT("APPLY_SUBMIT", "发起申请"),
// 部门审批节点
DEPT_APPROVE("DEPT_APPROVE", "部门审批"),
// 人事审批节点
HR_APPROVE("HR_APPROVE", "人事审批"),
// 最终确认节点
FINAL_CONFIRM("FINAL_CONFIRM", "最终确认");
/**
* key:数据库中的存储主键
* @EnumValue - MyBatis-Plus 注解,标记此字段为持久化到数据库的值
*/
@EnumValue
private final String key;
/**
* value:节点的中文描述
*/
private final String value;
/**
* logicType:逻辑标识,用于序列化到 JSON,前端、接口统一识别类型
* 该字段直接赋初值,不需参与构造
*/
private final String logicType = "enums";
/**
* Jackson 自定义序列化方法
* @JsonValue 指定序列化时使用该方法的返回值,序列化为完整对象
* 格式如:{ "key": "APPLY_SUBMIT", "value": "发起申请", "logicType": "enums" }
*/
@JsonValue
public Map<String, Object> toJson() {
Map<String, Object> map = new HashMap<>();
map.put("key", key);
map.put("value", value);
map.put("logicType", logicType);
return map;
}
/**
* Jackson 自定义反序列化工厂方法
* @JsonCreator 指定反序列化为枚举时用该静态方法
* 支持从完整 JSON 对象中(含 key/value/logicType)恢复枚举实例(主要根据 key 匹配)
* @param obj JSON 反序列化后的 Map 对象
* @return 匹配到的枚举实例;未匹配则返回 null(可根据业务需要调整为抛异常或默认值)
*/
@JsonCreator
public static ProcessNodeEnum fromJson(Map<String, Object> obj) {
if (obj == null || obj.get("key") == null) return null;
String k = obj.get("key").toString();
return Stream.of(ProcessNodeEnum.values())
.filter(e -> e.getKey().equals(k))
.findFirst()
.orElse(null);
}
}
b.ProcessNodeEnumTypeHandler自定义类型处理器
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.CallableStatement;
import java.sql.SQLException;
import java.util.stream.Stream;
/**
* -----------------------------------------------------------------------------------
* ProcessNodeEnumTypeHandler:用于数据库与 ProcessNodeEnum 枚举的双向映射
* 说明:
* 1. setNonNullParameter 用于向数据库写入(如插入、更新),仅写入枚举的 key(字符串)。
* 2. getNullableResult 用于从数据库读取 key,并将 key 转回对应的枚举实例。提供多种重载(By列名、By列索引、By存储过程)。
* 3. 当数据库中未查到匹配 key,返回 null。
* 4. 推荐在 MyBatis 配置或注解中为字段显式指定该 TypeHandler。
* -----------------------------------------------------------------------------------
*/
public class ProcessNodeEnumTypeHandler extends BaseTypeHandler<ProcessNodeEnum> {
/**
* 数据库写操作(插入和更新):将枚举类型字段的 key 写入 PreparedStatement。
* @param ps 编译好的 SQL 语句
* @param i 占位符索引
* @param parameter 当前参数(枚举实例,已保证非 null)
* @param jdbcType JDBC 类型(可选)
* @throws SQLException 写入过程异常由外抛出
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, ProcessNodeEnum parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.getKey()); // 只写 key 字段
}
/**
* 数据库读操作:通过列名获取枚举实例
* @param rs 结果集 ResultSet
* @param columnName 列名
* @return 匹配到的枚举实例,未匹配返回 null
* @throws SQLException 数据库操作异常
*/
@Override
public ProcessNodeEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
String key = rs.getString(columnName);
return getEnumByKey(key);
}
/**
* 数据库读操作:通过列索引获取枚举实例
* @param rs 结果集 ResultSet
* @param columnIndex 列索引
* @return 匹配到的枚举实例,未匹配返回 null
* @throws SQLException 数据库操作异常
*/
@Override
public ProcessNodeEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String key = rs.getString(columnIndex);
return getEnumByKey(key);
}
/**
* 存储过程读操作:通过列索引获取枚举实例
* @param cs CallableStatement
* @param columnIndex 列索引
* @return 匹配到的枚举实例,未匹配返回 null
* @throws SQLException 数据库操作异常
*/
@Override
public ProcessNodeEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String key = cs.getString(columnIndex);
return getEnumByKey(key);
}
/**
* 根据 key 匹配枚举实例的内部工具方法
* @param key 数据库返回的 key
* @return 匹配到的枚举实例,未找到则返回 null
*/
private ProcessNodeEnum getEnumByKey(String key) {
return Stream.of(ProcessNodeEnum.values())
.filter(e -> e.getKey().equals(key))
.findFirst()
.orElse(null); // 如果没有匹配项,则返回 null(防止抛异常)
}
}
03.自定义接口-OnbMapEnum
a.注解
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* MapEnum 接口:约定枚举必须提供一个数值型键(getKey)和一个文本型值(getValue)。
* 通过 JsonSerialize 和 JsonDeserialize 注解,绑定自定义的序列化/反序列化逻辑。
*
* @param <I> 枚举键的类型,继承自 Number,通常为 Integer 或 Long。
*/
@JsonSerialize(using = OnbMapEnumSerializer.class) // 序列化:枚举 -> JSON 对象
@JsonDeserialize(using = OnbMapEnumDeserializer.class) // 反序列化:JSON -> 枚举
public interface OnbMapEnum<I extends Number> {
/**
* 获取枚举的数字键,用于数据库存储及 JSON 传输中的 key 字段。
* 示例:1、2、3 等。
*
* @return 枚举对应的数字键
*/
int getKey();
/**
* 获取枚举的显示文本,用于前端展示或日志记录。
* 示例:"初中"、"大专" 等。
*
* @return 枚举对应的文本值
*/
String getValue();
}
b.序列化
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* MapEnumSerializer 实现:
* 1. 将入参对象强制转换为 MapEnum,确保可以调用 getKey() 和 getValue()。
* 2. 创建一个 Map,填充 "key"(数字键)、"value"(文本值)、"logicType"(固定值 "enums")。
* 3. 使用 JsonGenerator.writeObject 将整个 Map 写入 JSON 输出。
*
* 序列化示例输出:
* {
* "key": 4,
* "value": "大专",
* "logicType": "enums"
* }
*/
public class OnbMapEnumSerializer extends JsonSerializer {
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
@SuppressWarnings("unchecked")
OnbMapEnum<Number> mapEnum = (OnbMapEnum<Number>) value;
Map<String, Object> out = new HashMap<>(3);
out.put("key", mapEnum.getKey());
out.put("value", mapEnum.getValue());
out.put("logicType", "enums");
gen.writeObject(out);
}
}
c.反序列化
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
import org.springframework.beans.BeanUtils;
/**
* MapEnumDeserializer 实现:
* 1. 读取当前 JSON 属性的节点:可能是 ObjectNode、IntNode、TextNode 或 ArrayNode。
* 2. 尝试从节点中提取名为 "key" 的值,并转换成 Integer。
* 3. 通过 BeanUtils.findPropertyType 动态获取当前属性对应的枚举类类型。
* 4. 遍历该枚举类的所有常量,匹配 getKey() 与提取的 key 值,返回对应枚举实例。
* 5. 如果未匹配,则返回 null(可根据业务需求改为抛异常或返回默认枚举)。
*/
public class OnbMapEnumDeserializer extends JsonDeserializer<OnbMapEnum<Number>> {
/**
* 构造方法:可用于初始化日志或调试信息。
*/
protected OnbMapEnumDeserializer() {
// 仅示例打印,实际可移除或改为日志输出
System.out.println("初始化 MapEnumDeserializer");
}
@Override
public OnbMapEnum<Number> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 1. 将整个 JSON 内容读取为通用树节点
TreeNode treeNode = p.getCodec().readTree(p);
Integer key = null;
// 2. 根据节点类型,提取 key 值
if (treeNode instanceof ObjectNode) {
// 对象格式:{ "key": 4, "value": "大专", "logicType": "enums" }
ObjectNode obj = (ObjectNode) treeNode;
// 尝试以整数形式读取
key = obj.get("key").asInt();
if (key == null) {
// 当 asInt() 返回 null 时,再尝试以文本形式读取并转换
key = Integer.valueOf(obj.get("key").asText());
}
} else if (treeNode instanceof IntNode) {
// 纯数字格式:4
key = ((IntNode) treeNode).asInt();
} else if (treeNode instanceof TextNode) {
// 文本数字格式:"4"
key = Integer.valueOf(((TextNode) treeNode).asText());
} else if (treeNode instanceof ArrayNode) {
// 数组格式:[ { "key": 4 } ]
ArrayNode array = (ArrayNode) treeNode;
key = array.findValue("key").asInt();
}
// 3. 获取当前 JSON 属性名及其所属对象,用于反射获取字段类型
String propName = p.getCurrentName();
Class<?> parentClass = ctxt.getParser().getCurrentValue().getClass();
@SuppressWarnings("unchecked")
// 4. 通过 BeanUtils 查找父对象中名为 propName 的字段类型,转换为 MapEnum 子类型
Class<? extends OnbMapEnum<Number>> enumClass =
(Class<? extends OnbMapEnum<Number>>) BeanUtils.findPropertyType(propName, new Class<?>[]{parentClass});
// 5. 遍历枚举常量,匹配 key
for (OnbMapEnum<Number> constant : enumClass.getEnumConstants()) {
if (constant.getKey() == key) {
return constant;
}
}
// 6. 未找到匹配时返回 null(可替换逻辑)
return null;
}
}
d.自定义类型处理器
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
/**
* OnbMapEnumHandler 是一个 MyBatis 通用的枚举类型处理器
* 适用于实现了 OnbMapEnum 接口,且用整型 key 作为映射值的枚举类型。
*
* 主要职责:
* 1. 设置非空参数时,将枚举的 key 作为整数写入 PreparedStatement。
* 2. 从 JDBC 查询结果集 ResultSet 或 CallableStatement 中读取整数类型 key,
* 并通过枚举常量中的 key 进行匹配返回对应枚举实例。
* 3. 未匹配到对应枚举时返回 null。
*
* @param <E> 继承自 Enum 且实现 OnbMapEnum 接口的枚举类型
*/
public class OnbMapEnumHandler<E extends Enum<E> & OnbMapEnum> extends BaseTypeHandler<E> {
/**
* 枚举类型的 Class 对象,用于获取所有枚举实例
*/
private final Class<E> type;
/**
* 构造器,传入枚举类型 Class
* @param type 枚举的Class类型,不能为空
* @throws IllegalArgumentException 当 type 为 null 时抛出
*/
public OnbMapEnumHandler(Class<E> type) {
if (type == null) throw new IllegalArgumentException("Type argument cannot be null!");
this.type = type;
}
/**
* 设置非空参数,将枚举的 key 值写入 PreparedStatement
* @param ps PreparedStatement
* @param i 参数索引
* @param parameter 枚举实例
* @param jdbcType JDBC类型
* @throws SQLException JDBC异常
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getKey());
}
/**
* 根据列名从 ResultSet 获取整数 key,然后转换为对应枚举实例
* @param rs ResultSet
* @param columnName 列名
* @return 枚举实例或 null
* @throws SQLException JDBC异常
*/
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
int key = rs.getInt(columnName);
return enumByKey(key);
}
/**
* 根据列索引从 ResultSet 获取整数 key,然后转换为对应枚举实例
* @param rs ResultSet
* @param columnIndex 列索引
* @return 枚举实例或 null
* @throws SQLException JDBC异常
*/
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int key = rs.getInt(columnIndex);
return enumByKey(key);
}
/**
* 根据列索引从 CallableStatement 获取整数 key,然后转换为对应枚举实例
* @param cs CallableStatement
* @param columnIndex 列索引
* @return 枚举实例或 null
* @throws SQLException JDBC异常
*/
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int key = cs.getInt(columnIndex);
return enumByKey(key);
}
/**
* 辅助方法,通过整型 key 查找对应枚举实例
* @param key 枚举对应的整数 key
* @return 枚举实例或 null
*/
private E enumByKey(int key) {
for (E e : type.getEnumConstants()) {
if (e.getKey() == key) return e;
}
return null;
}
}
04.自定义接口-OnbStringKeyMapEnum
a.注解
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* StringKeyMapEnum 接口:约定枚举必须提供一个字符串键(getKey)和一个文本值(getValue)。
* 新版本序列化为完整对象格式:{"logicType": "enums", "value": "汉族", "key": "01"}
*/
@JsonSerialize(using = OnbStringKeyMapEnumSerializer.class) // 序列化:枚举 -> 完整JSON对象
@JsonDeserialize(using = OnbStringKeyMapEnumDeserializer.class) // 反序列化:多种格式 -> 枚举
public interface OnbStringKeyMapEnum {
/**
* 获取枚举在 JSON 中的字符串键。
* @return 枚举对应的字符串键
*/
String getKey();
/**
* 获取枚举的文本展示值。
* @return 枚举对应的显示文本
*/
String getValue();
}
b.序列化
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* StringKeyMapEnumSerializer 实现:
* 1. 将入参对象强制转换为 StringKeyMapEnum,确保可以调用 getKey() 和 getValue()。
* 2. 创建一个 Map,填充 "key"(字符串键)、"value"(文本值)、"logicType"(固定值 "enums")。
* 3. 使用 JsonGenerator.writeObject 将整个 Map 写入 JSON 输出。
*
* 序列化示例输出:
* {
* "key": "01",
* "value": "汉族",
* "logicType": "enums"
* }
*/
public class OnbStringKeyMapEnumSerializer extends JsonSerializer<OnbStringKeyMapEnum> {
@Override
public void serialize(OnbStringKeyMapEnum value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
Map<String, Object> result = new HashMap<>(3);
result.put("logicType", "enums");
result.put("value", value.getValue());
result.put("key", value.getKey());
gen.writeObject(result);
}
}
c.反序列化
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
import org.springframework.beans.BeanUtils;
/**
* StringKeyMapEnumDeserializer 实现:
* 1. 读取 JSON 为 TreeNode。
* 2. 支持多种格式:
* - 完整对象:{"logicType": "enums", "value": "汉族", "key": "01"}
* - 纯文本: "01"
* - 数组: [{"key": "01"}]
* 3. 获取当前属性名与父对象类型,通过 BeanUtils 查找字段实际类型(枚举类)。
* 4. 遍历枚举常量,采用 String.equals 比对 key,并返回匹配常量。
* 5. 未匹配时返回 null(或抛异常/默认值)。
*/
public class OnbStringKeyMapEnumDeserializer extends JsonDeserializer<OnbStringKeyMapEnum> {
protected OnbStringKeyMapEnumDeserializer() {
// 空构造,框架反射调用
}
@Override
@SuppressWarnings("unchecked")
public OnbStringKeyMapEnum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode treeNode = p.getCodec().readTree(p);
String key = null;
if (treeNode instanceof ObjectNode) {
// 支持完整对象格式:{"logicType": "enums", "value": "汉族", "key": "01"}
ObjectNode objNode = (ObjectNode) treeNode;
if (objNode.has("key")) {
key = objNode.get("key").asText();
}
} else if (treeNode instanceof TextNode) {
// 支持纯文本格式:"01"
key = ((TextNode) treeNode).asText();
} else if (treeNode instanceof ArrayNode) {
// 支持数组格式:[{"key": "01"}]
key = ((ArrayNode) treeNode).findValue("key").asText();
}
// 如果无法提取key,返回null
if (key == null) {
return null;
}
String propName = p.getCurrentName();
Class<?> parent = ctxt.getParser().getCurrentValue().getClass();
Class<? extends OnbStringKeyMapEnum> enumClass =
(Class<? extends OnbStringKeyMapEnum>) BeanUtils.findPropertyType(propName, new Class<?>[]{parent});
// 遍历枚举常量,匹配key
for (OnbStringKeyMapEnum constant : enumClass.getEnumConstants()) {
if (constant.getKey().equals(key)) {
return constant;
}
}
return null;
}
}
d.自定义类型处理器
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
/**
* OnbStringKeyMapEnumHandler 是一个 MyBatis 通用的枚举类型处理器
* 适用于实现了 OnbStringKeyMapEnum 接口,且用字符串 key 作为映射值的枚举类型。
*
* 主要职责:
* 1. 设置非空参数时,将枚举的 key 作为字符串写入 PreparedStatement。
* 2. 从 JDBC 查询结果集 ResultSet 或 CallableStatement 中读取字符串类型 key,
* 并通过枚举常量中的 key 进行匹配返回对应枚举实例。
* 3. 未匹配到对应枚举时返回 null。
*
* @param <E> 继承自 Enum 且实现 OnbStringKeyMapEnum 接口的枚举类型
*/
public class OnbStringKeyMapEnumHandler<E extends Enum<E> & OnbStringKeyMapEnum> extends BaseTypeHandler<E> {
/**
* 枚举类型的 Class 对象,用于获取所有枚举实例
*/
private final Class<E> type;
/**
* 构造器,传入枚举类型 Class
* @param type 枚举的Class类型,不能为空
* @throws IllegalArgumentException 当 type 为 null 时抛出
*/
public OnbStringKeyMapEnumHandler(Class<E> type) {
if (type == null) throw new IllegalArgumentException("Type argument cannot be null!");
this.type = type;
}
/**
* 设置非空参数,将枚举的字符串 key 值写入 PreparedStatement
* @param ps PreparedStatement
* @param i 参数索引
* @param parameter 枚举实例
* @param jdbcType JDBC类型
* @throws SQLException JDBC异常
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.getKey());
}
/**
* 根据列名从 ResultSet 获取字符串 key,然后转换为对应枚举实例
* @param rs ResultSet
* @param columnName 列名
* @return 枚举实例或 null
* @throws SQLException JDBC异常
*/
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
String key = rs.getString(columnName);
return enumByKey(key);
}
/**
* 根据列索引从 ResultSet 获取字符串 key,然后转换为对应枚举实例
* @param rs ResultSet
* @param columnIndex 列索引
* @return 枚举实例或 null
* @throws SQLException JDBC异常
*/
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String key = rs.getString(columnIndex);
return enumByKey(key);
}
/**
* 根据列索引从 CallableStatement 获取字符串 key,然后转换为对应枚举实例
* @param cs CallableStatement
* @param columnIndex 列索引
* @return 枚举实例或 null
* @throws SQLException JDBC异常
*/
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String key = cs.getString(columnIndex);
return enumByKey(key);
}
/**
* 辅助方法,通过字符串 key 查找对应枚举实例
* @param key 枚举对应的字符串 key
* @return 枚举实例或 null
*/
private E enumByKey(String key) {
for (E e : type.getEnumConstants()) {
if (e.getKey().equals(key)) return e;
}
return null;
}
}
05.全局自动注册实现-OnbMapEnum、OnbStringKeyMapEnum
a.说明
1.Spring 容器管理和初始化时机
该类被 @Configuration 注解标记为配置类,Spring 容器启动时会扫描并实例化它。
在实例化完成后,执行带有 @PostConstruct 注解的方法 registerEnumTypeHandlers(),确保注册逻辑在 Spring 和 MyBatis 配置都已初始化完成后运行。
-----------------------------------------------------------------------------------------------------
2.自动扫描枚举类
使用 Reflections 库对指定包(ENUM_SCAN_PACKAGE)进行扫描,找到所有实现了自定义接口 OnbMapEnum 和 OnbStringKeyMapEnum 的枚举类。
Reflections 通过反射机制遍历指定包下的所有类及其接口继承关系,无需手动维护枚举列表。
-----------------------------------------------------------------------------------------------------
3.判断并注册对应的 TypeHandler
对扫描到的每个实现接口的枚举类,先判断是否确实是枚举类型(enumClass.isEnum()),防止误注册非枚举类。
分别为两类枚举注册对应的自定义 TypeHandler 实例 OnbMapEnumHandler 和 OnbStringKeyMapEnumHandler。
注册时调用 MyBatis TypeHandlerRegistry 的 register() 方法,告知 MyBatis 对该枚举类数据库读写时应使用对应的 Handler。
-----------------------------------------------------------------------------------------------------
4.实现业务层的解耦和扩展便利
开发者只需让自定义枚举实现对应接口且放入指定包路径,无需额外配置 MyBatis xml 或代码。
程序启动时自动完成枚举与数据库类型之间的映射处理器注册,提升开发效率,避免遗漏和错误。
支持多个枚举接口,可灵活扩展更多枚举类型及对应处理器。
-----------------------------------------------------------------------------------------------------
5.简易的注册日志输出
注册完成后输出当前注册的处理器枚举数量,方便调试和确认功能生效。
b.代码
import java.util.Set;
import javax.annotation.PostConstruct;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
/**
* 全局自动注册实现 OnbMapEnum、OnbStringKeyMapEnum 的枚举 TypeHandler 配置
*/
@Configuration
public class OnbTypeHandlerAutoConfig {
// 你实际的包名(配置为你的根包即可,避免扫描不到自定义枚举)
private static final String ENUM_SCAN_PACKAGE = "com.khict.product.onboard.api.enums";
@Autowired
private SqlSessionFactory sqlSessionFactory;
@PostConstruct
public void registerEnumTypeHandlers() {
TypeHandlerRegistry registry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry();
// 扫描所有 implements OnbMapEnum
Reflections reflections = new Reflections(ENUM_SCAN_PACKAGE);
Set<Class<? extends OnbMapEnum>> mapEnums = reflections.getSubTypesOf(OnbMapEnum.class);
for (Class<? extends OnbMapEnum> enumClass : mapEnums) {
// 检查是枚举类
if (enumClass.isEnum()) {
registry.register(enumClass, new OnbMapEnumHandler(enumClass));
}
}
// 扫描所有 implements OnbStringKeyMapEnum
Set<Class<? extends OnbStringKeyMapEnum>> stringEnums = reflections.getSubTypesOf(OnbStringKeyMapEnum.class);
for (Class<? extends OnbStringKeyMapEnum> enumClass : stringEnums) {
if (enumClass.isEnum()) {
registry.register(enumClass, new OnbStringKeyMapEnumHandler(enumClass));
}
}
System.out.println("=== 注册枚举TypeHandler ===");
System.out.println("注册 OnbMapEnumHandler 的枚举类型数量:" + mapEnums.size());
System.out.println("注册 OnbStringKeyMapEnumHandler 的枚举类型数量:" + stringEnums.size());
}
}
3.8 枚举:EnumManager
01.背景与问题分析
a.数据类型不一致
部分枚举(如`deletedFlag`)的 `key` 为数字 `0/1`,
而另一些(如`maritalStatus`)为字符串 `"01"`、`"02"`,这导致在表单绑定和数据比较时极易出错。
b.逻辑高度分散
枚举的转换和获取逻辑散落在各个组件和方法中,没有统一出口,导致代码重复、难以维护。
c.转换职责不清
在表单显示和提交时,开发人员需要手动处理枚举值与后端API所需格式的转换,增加了心智负担和潜在BUG。
02.EnumManager方案
a.统一管理中心 (`EnumManager` 类)
这是一个独立的工具类,作为所有枚举数据的唯一权威来源 (Single Source of Truth)。它的主要职责是:
在初始化时,将所有来源不一的枚举数据**规范化**,例如将所有 `key` 统一处理为字符串,构建一个稳定、可预测的内部数据源。
对外提供简洁、统一的方法,如 `getOptions()`(获取下拉框选项)和 `getLabel()`(根据 key 获取显示文本)。
b.配置驱动 (`ENUM_TYPE_CONFIG`)
我们引入一个独立的配置文件,用来定义每种枚举的转换策略。这使得我们的方案具备高度的灵活性和可扩展性。
-----------------------------------------------------------------------------------------------------
// 通过配置,清晰地定义了每种枚举的前后端类型差异
const ENUM_TYPE_CONFIG = {
maritalStatus: { keyType: 'string', apiType: 'string' },
deletedFlag: { keyType: 'string', apiType: 'number' } // 前端用字符串,提交给API时转为数字
};
c.双向数据管道
我们为表单数据流设计了清晰的双向处理管道,彻底分离了数据转换的职责
API -> 前端:从后端获取数据后,通过 `normalizeDataForForm()` 管道,将数据(如数字`1`)转换为前端表单需要的格式(如字符串`"1"`)。
前端 -> API:在表单提交前,通过 `normalizeDataForAPI()` 管道,根据配置将数据(如字符串`"0"`)转换为后端API需要的格式(如数字`0`)。
d.无缝集成 (`useEnum` 组合式函数)
为了方便在 Vue 项目中使用,我们将其封装为一个 `useEnum` Composable。组件只需调用此函数,即可轻松获取格式化好的枚举数据和方法,无需关心底层实现。
03.EnumManager方案
a.代码
// =======================================================================
// 0. 模拟原始数据源 (相当于项目中的常量或从后端获取的原始枚举)
// 问题:key 的类型不一致 (字符串、数字)
// =======================================================================
const RAW_ENUM_DATA = {
maritalStatusOptions: {
"01": "未婚",
"02": "已婚",
"03": "离异",
"04": "丧偶"
},
needAccommodationOptions: {
"0": "否",
"1": "是"
},
deletedFlagOptions: {
0: "正常",
1: "已删除"
}
};
// =======================================================================
// 2. 数据转换策略配置 (完全按照您的设计)
// 定义了每种枚举的目标类型和格式
// =======================================================================
const ENUM_TYPE_CONFIG = {
maritalStatus: { keyType: 'string', apiType: 'string' }, // key是字符串"01", API也需要"01"
needAccommodation: { keyType: 'string', apiType: 'string' }, // key是字符串"0", API也需要"0"
deletedFlag: { keyType: 'string', apiType: 'number' } // key规范为字符串"0", 但提交到API时需要数字 0
};
// =======================================================================
// 1. 创建枚举数据管理中心 (EnumManager Class)
// =======================================================================
class EnumManager {
constructor(rawData, config) {
this.config = config;
this.normalizedData = {}; // 用于存储规范化后的数据 { maritalStatus: { '01': '未婚', ... } }
this._initialize(rawData);
console.log("✅ EnumManager 初始化完成,所有枚举 key 已规范为字符串。");
console.log(" 规范化后的内部数据:", this.normalizedData);
}
/**
* 内部方法:初始化并规范化所有枚举数据
* @param {object} rawData - 原始枚举数据
*/
_initialize(rawData) {
// 将 maritalStatusOptions -> maritalStatus
const getBaseName = (key) => key.replace(/Options$/, '');
for (const rawKey in rawData) {
const enumType = getBaseName(rawKey); // 'maritalStatus'
if (this.config[enumType]) {
const rawOptions = rawData[rawKey];
this.normalizedData[enumType] = {};
for (const optionKey in rawOptions) {
// 核心:所有 key 统一转换为字符串类型
const normalizedKey = String(optionKey);
this.normalizedData[enumType][normalizedKey] = rawOptions[optionKey];
}
}
}
}
/**
* 获取规范化后的枚举选项 (用于前端Select/Radio组件)
* @param {string} enumType - 枚举类型, e.g., 'maritalStatus'
* @returns {Array<{value: string, label: string}>}
*/
getEnumOptions(enumType) {
const options = this.normalizedData[enumType];
if (!options) return [];
return Object.keys(options).map(key => ({
value: key, // value 保证是字符串
label: options[key]
}));
}
/**
* 获取枚举显示值
* @param {string} enumType - 枚举类型
* @param {string | number} key - 需要查找的 key (可以是数字或字符串)
* @returns {string} - 对应的标签文本
*/
getEnumLabel(enumType, key) {
if (key === null || key === undefined) return '';
const options = this.normalizedData[enumType];
if (!options) return `[未知枚举: ${enumType}]`;
// 核心:查找时也对 key 进行规范化,抹平输入差异
const normalizedKey = String(key);
return options[normalizedKey] || `[未知选项: ${key}]`;
}
}
// =======================================================================
// 4. Composition API 集成 (模拟 useEnum Hook)
// =======================================================================
function useEnum(manager) {
// 在真实 Vue 项目中, manager 实例通常是单例的
const getOptions = (enumType) => manager.getEnumOptions(enumType);
const getLabel = (enumType, key) => manager.getEnumLabel(enumType, key);
return { getOptions, getLabel };
}
// =======================================================================
// 3. 表单数据处理管道 (模拟实现)
// =======================================================================
/**
* 将从 API 获取的数据规范化,以适配前端表单
* @param {object} apiData - 从后端接口收到的数据
*/
function normalizeDataForForm(apiData) {
// 假设后端为 deletedFlag 返回了数字 1
console.log("\n管道(<-): 正在将 API 数据适配到表单...");
console.log(" 转换前 (API Data):", apiData);
const formData = { ...apiData };
// 策略:确保所有枚举值都符合前端的字符串标准
if (formData.deletedFlag !== undefined) {
formData.deletedFlag = String(formData.deletedFlag);
}
// 其他字段...
console.log(" 转换后 (Form Data):", formData);
return formData;
}
/**
* 提交表单前,将前端数据转换为 API 期望的格式
* @param {object} formData - 从表单收集的数据
* @param {object} config - 转换策略配置
*/
function normalizeDataForAPI(formData, config) {
console.log("\n管道(->): 正在将表单数据转换为 API 格式...");
console.log(" 转换前 (Form Data):", formData);
const apiPayload = { ...formData };
for (const key in apiPayload) {
if (config[key] && config[key].apiType === 'number') {
apiPayload[key] = Number(apiPayload[key]);
}
// 可以添加其他转换逻辑,如 'boolean' 等
}
console.log(" 转换后 (API Payload):", apiPayload);
return apiPayload;
}
// =======================================================================
// ========================= DEMO 运行区域 ===============================
// =======================================================================
// 1. 创建 EnumManager 实例
const enumManager = new EnumManager(RAW_ENUM_DATA, ENUM_TYPE_CONFIG);
// 2. 模拟 useEnum Hook
const { getOptions, getLabel } = useEnum(enumManager);
console.log("\n------------------ 前端使用场景 ------------------");
// 场景一:为下拉框提供选项
const maritalStatusSelectOptions = getOptions('maritalStatus');
console.log("\n获取'婚姻状况'的下拉框选项:", maritalStatusSelectOptions);
/* 输出:
[
{ value: '01', label: '未婚' },
{ value: '02', label: '已婚' },
{ value: '03', label: '离异' },
{ value: '04', label: '丧偶' }
]
*/
// 场景二:在表格中显示标签,即使传入的是数字也能正确匹配
const deletedFlagLabel1 = getLabel('deletedFlag', 1); // 传入数字
const deletedFlagLabel2 = getLabel('deletedFlag', "0"); // 传入字符串
console.log(`\n获取'删除标志'的标签 (key=1): "${deletedFlagLabel1}"`); // "正常"
console.log(`获取'删除标志'的标签 (key="0"): "${deletedFlagLabel2}"`); // "已删除" -> 修正:应该是 “正常”
// 场景三:处理数据管道
console.log("\n------------------ 数据管道场景 ------------------");
// 从后端加载数据
const dataFromAPI = {
name: "张三",
maritalStatus: "01",
deletedFlag: 1 // API 返回的是数字
};
const formData = normalizeDataForForm(dataFromAPI);
// 假设用户在表单中修改了数据
formData.deletedFlag = "0"; // 表单组件 v-model 绑定的是字符串 "0"
// 用户点击提交,数据进入API转换管道
const payloadForAPI = normalizeDataForAPI(formData, ENUM_TYPE_CONFIG);
/*
b.输出
管道(<-): 正在将 API 数据适配到表单...
转换前 (API Data): { name: '张三', maritalStatus: '01', deletedFlag: 1 }
转换后 (Form Data): { name: '张三', maritalStatus: '01', deletedFlag: '1' }
管道(->): 正在将表单数据转换为 API 格式...
转换前 (Form Data): { name: '张三', maritalStatus: '01', deletedFlag: '0' }
转换后 (API Payload): { name: '张三', maritalStatus: '01', deletedFlag: 0 }
01.方式1:注解
a.import
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
b.Date
@DateTimeFormat(pattern = "yyyy-MM-dd") // 用于表单绑定
@JSONField(format = "yyyy-MM-dd") // Fastjson序列化,用于JSON处理
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") // Jackson序列化,用于JSON处理
private Date dischargeDate;
c.Date
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") // 用于表单绑定
@JSONField(format = "yyyy-MM-dd HH:mm:ss") // Fastjson序列化,用于JSON处理
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") // Jackson序列化,用于JSON处理
private Date updateTime;
02.方式2:配置DateFormatConfig
a.配置类:yml
# ===============================================================
# 自定义日期格式映射配置 (DateFormatConfig.class)
# ===============================================================
date-format:
patterns:
# 仅日期格式的字段(支持通配符)
date-only:
- birthDate
- dischargeDate
- availableDate
- fillDate
- "*Date" # 通配符:以Date结尾
- "*Birth*" # 通配符:包含Birth
- "expire*" # 通配符:以expire开头
# 日期时间格式的字段
date-time:
- createTime
- updateTime
- "*Time" # 通配符:以Time结尾
- "*DateTime" # 通配符:以DateTime结尾
- "start*" # 通配符:以start开头
- "end*" # 通配符:以end开头
formats:
date-only: "yyyy-MM-dd"
date-time: "yyyy-MM-dd HH:mm:ss"
b.配置类:注册
@SpringBootApplication
@ComponentScan("cn.myslayers.**")
@MapperScan("cn.myslayers.**.mapper")
@EnableConfigurationProperties(DateFormatConfig.class) // 注册 DateFormatConfig 配置类
public class LibraOnboardServerApplication {
public static void main(String[] args) {
SpringApplication.run(LibraOnboardServerApplication.class, args);
}
}
c.配置类:书写
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* DateFormatConfig 配置类
*
* <p>该配置类实现基于字段名匹配的日期格式动态序列化和反序列化。
* 通过 application.yml 中前缀为 "date-format" 的配置,载入字段名匹配规则 patterns
* 和格式映射 formats,实现在 JSON 序列化和反序列化过程中,根据字段名灵活切换日期格式。
*
* <p>序列化时:针对字段名匹配的日期格式,格式化 java.util.Date 为字符串。
*
* <p>反序列化时:实现 ContextualDeserializer,可以根据字段名上下文使用对应格式解析字符串为 Date;
* 解决默认全局统一格式带来的日期字符串解析失败问题。
*
* <p>该方案避免破坏 Spring Boot 的默认 Jackson 配置,将自定义序列化器和反序列化器包裹在自定义模块中,
* 由 Spring 容器自动发现并注入到全局 ObjectMapper 中,实现扩展而非替换。
*
* <p>示例配置(简化版):
* <pre>
* date-format:
* patterns:
* date-only:
* - "*Date"
* date-time:
* - "*Time"
* formats:
* date-only: "yyyy-MM-dd"
* date-time: "yyyy-MM-dd HH:mm:ss"
* </pre>
*
* <p>为什么需要在主启动类添加 {@code @EnableConfigurationProperties(DateFormatConfig.class)}
*
* <p>要让 Spring Boot 将 {@code DateFormatConfig} 中用 {@code @ConfigurationProperties(prefix = "date-format")}
* 标注的属性与 {@code application.yml}(或 {@code application.properties}) 中对应的配置绑定起来,
* 必须将该类注册为一个 Configuration Properties Bean。单纯在类上加上 {@code @Configuration}
* 并不足以触发属性绑定,其职责仅是将该类作为一个普通的配置类(含 {@code @Bean} 方法)加载到容器中。
*
* <p>1. {@code @ConfigurationProperties} 并不隐含组件扫描
* {@code @Configuration} 可以将带该注解的类纳入 Spring 容器进行管理,但不会对其中的
* {@code @ConfigurationProperties} 注解做属性值绑定处理。Spring Boot 在启动时,会扫描
* 被 {@code @EnableConfigurationProperties} 或 {@code @ConfigurationPropertiesScan}
* 显式标记的类,并将配置文件中的值注入到这些类中。
*
* <p>2. {@code @EnableConfigurationProperties} 的作用
* - 将指定的 {@code @ConfigurationProperties} 类(如 {@code DateFormatConfig})注册为 Bean
* - 触发对该 Bean 中字段的松散绑定(support for relaxed binding)
* - 自动验证(如配合 JSR-303 验证注解时生效)
*
* <p>3. 如果仅使用 {@code @Configuration} 会缺少绑定
* <pre>
* @Configuration
* @ConfigurationProperties(prefix = "date-format")
* public class DateFormatConfig { … }
* </pre>
* 以上写法会让 Spring 加载这个配置类并执行其 {@code @Bean} 方法(如 {@code customDateModule()}),
* 但对 {@code patterns}、{@code formats} 等字段不会进行值注入;这些字段仍保持默认的空 Map,
* 导致序列化/反序列化逻辑中无法拿到用户在 {@code application.yml} 中的配置。
*
* <p>4. 正确做法
* 在主启动类或任意 {@code @Configuration} 类上添加:
* <pre>
* @SpringBootApplication
* @EnableConfigurationProperties(DateFormatConfig.class)
* public class Application { … }
* </pre>
* 或者使用 Spring Boot 2.2+ 提供的批量扫描:
* <pre>
* @SpringBootApplication
* @ConfigurationPropertiesScan("cn.myslayers.product.onboard.config")
* public class Application { … }
* </pre>
*
* <p>关键结论:
* - {@code @Configuration} 负责注册 Bean;
* - {@code @EnableConfigurationProperties}(或 {@code @ConfigurationPropertiesScan})负责绑定配置。
* 二者缺一不可,才能让带 {@code @ConfigurationProperties} 的类既被托管,又能正确注入配置值。
*/
@Configuration
@ConfigurationProperties(prefix = "date-format")
@Data
public class DateFormatConfig {
/**
* 字段名匹配规则,key 为规则名称,value 为字段名或通配符列表
* 用于在序列化或反序列化时,根据字段名匹配对应格式
*/
private Map<String, List<String>> patterns = new HashMap<>();
/**
* 每个匹配规则对应的日期格式字符串,如 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss
*/
private Map<String, String> formats = new HashMap<>();
/**
* 返回封装了自定义序列化器和反序列化器的 Jackson SimpleModule Bean。
* Spring Boot 自动检测并注册该模块到全局 ObjectMapper,实现增强功能。
*
* @return 包含自定义日期序列化和反序列化逻辑的 Jackson 模块
*/
@Bean
public SimpleModule customDateModule() {
SimpleModule module = new SimpleModule();
// 添加 Date 类型序列化器,负责根据字段名格式化日期输出
module.addSerializer(Date.class, new DateSerializer(this));
// 添加 Date 类型反序列化器,基于字段名动态匹配解析格式
module.addDeserializer(Date.class, new DateDeserializer(this));
return module;
}
/**
* 自定义日期序列化器
*
* <p>根据当前序列化字段名匹配对应日期格式,格式化日期为字符串输出。
* 使用线程安全的 SimpleDateFormat 缓存,保证多线程环境下安全。
*/
public static class DateSerializer extends JsonSerializer<Date> {
/** 配置对象,用于获取字段匹配规则及格式化字符串 */
private final DateFormatConfig config;
/** SimpleDateFormat 缓存,key 为格式字符串,value 为对应的格式化实例 */
private final Map<String, SimpleDateFormat> formatterCache = new ConcurrentHashMap<>();
/**
* 构造方法,注入配置实例
*
* @param config DateFormatConfig 实例,用于读取匹配配置
*/
public DateSerializer(DateFormatConfig config) {
this.config = config;
}
/**
* 序列化实现
*
* @param date java.util.Date 日期对象
* @param gen JsonGenerator,用于写出序列化内容
* @param serializers 序列化上下文(未使用)
* @throws IOException IO异常
*/
@Override
public void serialize(Date date, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (date == null) {
gen.writeNull();
return;
}
// 获取当前序列化的 JSON 字段名
String fieldName = gen.getOutputContext().getCurrentName();
// 根据字段名匹配合适的日期格式
String pattern = determinePattern(fieldName);
// 从缓存获取对应的 SimpleDateFormat 实例,若不存在则创建并缓存
SimpleDateFormat sdf = formatterCache.computeIfAbsent(pattern, p -> {
SimpleDateFormat format = new SimpleDateFormat(p);
format.setTimeZone(TimeZone.getTimeZone("GMT+8"));
return format;
});
// SimpleDateFormat 线程不安全,序列化时使用同步保证安全
synchronized (sdf) {
gen.writeString(sdf.format(date));
}
}
/**
* 根据字段名在配置的 patterns 中匹配对应格式
*
* @param fieldName JSON 字段名,可能为 null
* @return 匹配的日期格式字符串(若匹配失败,默认返回 date-time 格式)
*/
private String determinePattern(String fieldName) {
if (fieldName == null) {
return config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
}
// 优先寻找 date-only 模式匹配
for (String p : config.patterns.getOrDefault("date-only", Collections.emptyList())) {
if (matchPattern(fieldName, p)) {
return config.formats.getOrDefault("date-only", "yyyy-MM-dd");
}
}
// 再寻找 date-time 模式匹配
for (String p : config.patterns.getOrDefault("date-time", Collections.emptyList())) {
if (matchPattern(fieldName, p)) {
return config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
}
}
// 默认格式返回
return config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
}
/**
* 字段名与模式简单通配符匹配,支持 * 替代任意字符
*
* @param fieldName 字段名
* @param pattern 匹配模式,可包含 *
* @return 是否匹配
*/
private boolean matchPattern(String fieldName, String pattern) {
if (pattern.contains("*")) {
// 转换 * 为正则表达式 ".*"
String regex = pattern.replace("*", ".*");
return fieldName.matches(regex);
}
return fieldName.equals(pattern);
}
}
/**
* 自定义日期反序列化器,支持上下文感知,可根据 JSON 字段名动态选择解析日期格式。
*
* <p>通过实现 Jackson 的 ContextualDeserializer 接口:
* 反序列化过程中可获得当前字段信息,基于此选择对应的日期格式解析字符串。
*
* <p>线程安全的 SimpleDateFormat 缓存实现,确保多线程环境安全。
*/
public static class DateDeserializer extends JsonDeserializer<Date> implements ContextualDeserializer {
/** 配置对象,用于获取匹配规则 */
private final DateFormatConfig config;
/** 当前字段对应的日期格式字符串 */
private String pattern;
/** SimpleDateFormat 缓存 */
private final Map<String, SimpleDateFormat> formatterCache = new ConcurrentHashMap<>();
/** 无参构造,默认未绑定配置和格式 */
public DateDeserializer() {
this.config = null;
this.pattern = null;
}
/**
* 构造函数,绑定配置但未指定具体格式
*/
public DateDeserializer(DateFormatConfig config) {
this.config = config;
this.pattern = null;
}
/**
* 构造函数,绑定配置和具体格式,用于上下文创建返回具体格式的实例
*
* @param config 配置对象
* @param pattern 具体日期格式字符串
*/
public DateDeserializer(DateFormatConfig config, String pattern) {
this.config = config;
this.pattern = pattern;
}
/**
* 反序列化实现
*
* @param p JsonParser
* @param ctxt 反序列化上下文
* @return 解析后的 Date 对象
* @throws IOException 解析异常抛出
* @throws JsonProcessingException JSON 处理异常
*/
@Override
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String text = p.getText();
if (text == null || text.trim().isEmpty()) {
return null;
}
// 使用上下文中匹配的格式,默认回退全局 date-time 格式
String parsePattern = pattern != null ? pattern : config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
SimpleDateFormat sdf = formatterCache.computeIfAbsent(parsePattern, ptn -> {
SimpleDateFormat format = new SimpleDateFormat(ptn);
format.setTimeZone(TimeZone.getTimeZone("GMT+8"));
return format;
});
try {
synchronized (sdf) {
return sdf.parse(text);
}
} catch (ParseException e) {
throw new IOException("Failed to parse Date value '" + text + "' with pattern '" + parsePattern + "'", e);
}
}
/**
* ContextualDeserializer 回调,基于 BeanProperty 获得当前 JSON 字段信息,
* 然后匹配规则,返回新的带有具体格式的 DateDeserializer 实例。
*
* @param ctxt 反序列化上下文
* @param property 当前字段属性信息
* @return 具体字段匹配的 DateDeserializer 实例
* @throws JsonMappingException 反序列化映射异常
*/
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
if (property != null && config != null) {
String fieldName = property.getName();
String matchedPattern = determinePattern(fieldName);
return new DateDeserializer(config, matchedPattern);
}
return this;
}
/**
* 同序列化器匹配逻辑,优先匹配 date-only,再匹配 date-time,未匹配使用默认。
*
* @param fieldName 当前字段名
* @return 日期格式字符串
*/
private String determinePattern(String fieldName) {
if (fieldName == null) {
return config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
}
for (String pattern : config.patterns.getOrDefault("date-only", Collections.emptyList())) {
if (matchPattern(fieldName, pattern)) {
return config.formats.getOrDefault("date-only", "yyyy-MM-dd");
}
}
for (String pattern : config.patterns.getOrDefault("date-time", Collections.emptyList())) {
if (matchPattern(fieldName, pattern)) {
return config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
}
}
return config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
}
/**
* 简单通配符匹配,支持 "*" 替代任意字符序列。
*
* @param fieldName 字段名
* @param pattern 模式,可能包含 *
* @return 是否匹配
*/
private boolean matchPattern(String fieldName, String pattern) {
if (pattern.contains("*")) {
String regex = pattern.replace("*", ".*");
return fieldName.matches(regex);
}
return fieldName.equals(pattern);
}
}
}
4 开源项目
4.1 项目配置
01.端口规范
a.基准端口
20000
b.环境隔离
a.开发 (Dev) 环境
20xxx
b.测试 (Test) 环境
21xxx (Dev 端口 + 1000)
c.生产 (Prod) 环境
22xxx (Dev 端口 + 2000)
c.模块分组
a.网关
x0000
b.示例
x01xx
c.基建/系统
x02xx
d.业务
x03xx
e.模版
x04xx
02.端口管理
模块分组 服务名称 开发环境端口 (Dev) 测试环境端口 (Test) 生产环境端口 (Prod) 备注
网关 (Gateway) libra-moudle-gateway 20000 21000 22000 核心入口,端口号固定且易记
框架示例 (Framework Demos) libra-framework-demo 20100 21100 22100 201xx 段为框架 Demos 预留
libra-framework-mybatis-demo 20101 21101 22101
libra-framework-office-demo 20102 21102 22102
libra-framework-redis-demo 20103 21103 22103
libra-framework-security-demo 20104 21104 22104
libra-framework-toolkit-demo 20105 21105 22105
libra-framework-websocket-demo 20106 21106 22106
基础设施 & 系统模块(Infra & System) libra-moudle-infra 20200 21200 22200 202xx 段为核心基础服务
libra-moudle-system 20201 21201 22201
业务模块 (Business) 核心业务区,按领域划分 10位段
Onboard 领域) libra-moudle-business-onboard 20310 21310 22310 Onboard 领域主服务 (x031x)
libra-moudle-business-template 20311 21311 22311 Template 是 Onboard 的子模块
未来 Order 领域) libra-moudle-business-order 20320 21320 22320 预留给 Order 领域 (x032x)
libra-moudle-business-order-item 20321 21321 22321 预留给订单详情子服务
未来 Payment 领域) libra-moudle-business-payment 20330 21330 22330 预留给 Payment 领域 (x033x)
03.网段管理
来源 协议 端口 策略 备注
自定义 TCP 1-65535 允许 放开全部IP段
自定义 TCP 8090 允许 1Panel Linux 面板默认端口
HTTP (80) TCP 80 允许 Web服务HTTP (80),如 Apache、Nginx
HTTPS (443) TCP 443 允许 Web服务HTTPS (443),如 Apache、Nginx
Linux 登录(22) TCP 22 允许 Linux SSH登录
Windows登录(3389) TCP 3389 允许 Windows远程桌面登录
Windows登录优化(3389) UDP 3389 允许 Windows远程桌面登录优化
Ping ICMP ALL 允许 通过Ping测试网络连通性 (放通ALL ICMP)
04.防火墙管理
firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="0.0.0.0/0" port protocol="tcp" port="1-65535" accept' --permanent
firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="0.0.0.0/0" port protocol="udp" port="1-65535" accept' --permanent
firewall-cmd --reload
4.2 排查信息
01.常见信息1
a.离线Nexus
删除_remote.repositories,Maven 3.x 引入的追踪文件(Tracking File)
-----------------------------------------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
<!-- 阿里云镜像(用于公共依赖) -->
<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
<!-- 关键:为私有 groupId 禁用远程仓库 -->
<profiles>
<profile>
<id>private-repos-disabled</id>
<repositories>
<!-- 显式声明 com.zkzx.layer 只从本地加载 -->
<repository>
<id>local-only</id>
<url>file://${user.home}/.m2/repository</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>local-only</id>
<url>file://${user.home}/.m2/repository</url>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>private-repos-disabled</activeProfile>
</activeProfiles>
</settings>
b.SQL校验
spring:
redis:
database: 2
port: 6379
host: 10.10.20.39
password: Tpccn@8710881
datasource:
druid:
# 初始连接数
initial-size: 1
min-idle: 1
max-active: 550
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://10.10.20.39:3306/sxlq_zhgh?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&skip-name-resolve=true&connectTimeout=5000&socketTimeout=30000
username: root
password: Tpccn@8710881
# 关闭连接验证
test-on-borrow: false
test-on-return: false
test-while-idle: false
validation-query: SELECT 1
4.3 DatabaseUtil
01.常见信息
a.功能
多数据源动态管理(Druid + 简单数据源双模式)
MyBatis 风格的 XML SQL 解析和执行
动态 SQL 支持(if、where、set、foreach、choose 等标签)
声明式事务管理(链式 API)
批量操作(批量插入、更新、删除、UPSERT)
跨数据库物理分页(MySQL、Oracle、H2、PostgreSQL)
自动 DTO/VO 映射(驼峰/下划线自动转换)
SQL 执行日志追踪(完整 SQL + 参数值)
线程级数据源切换
对象到 SQL 参数自动转换
ResultSet 到对象自动映射
XML SQL 文件自动扫描
b.源文件
pisces-boot-app/src/main/java/cn/myslayers/app/utils/
└── DatabaseUtil.java → 企业级数据库工具类(5534 行)
c.目标结构
libra-spring-boot-starter-database/
├── src/main/java/cn/myslayers/libra/framework/database/
│ ├── core/
│ │ ├── DataSourceManager.java # 数据源管理器
│ │ ├── DruidDataSourceFactory.java # Druid 连接池工厂
│ │ ├── SimpleDataSourceFactory.java # 简单数据源工厂
│ │ └── DatabaseConfig.java # 数据源配置类
│ ├── accessor/
│ │ ├── DatabaseAccessor.java # 数据库链式访问器
│ │ └── TransactionManager.java # 事务管理器
│ ├── executor/
│ │ ├── CommonSqlExecutor.java # 通用 SQL 执行器
│ │ ├── MyBatisSqlExecutor.java # MyBatis SQL 执行器
│ │ ├── BatchExecutor.java # 批量执行器
│ │ └── PaginationExecutor.java # 分页执行器
│ ├── parser/
│ │ ├── CommonSqlParser.java # 通用 SQL 解析器
│ │ ├── MyBatisSqlParser.java # MyBatis XML 解析器
│ │ ├── DynamicSqlConverter.java # 动态 SQL 转换器
│ │ └── XmlSqlLoader.java # XML SQL 加载器
│ ├── mapper/
│ │ ├── ObjectMapper.java # 对象映射器
│ │ ├── ObjectToParameterConverter.java # 对象转参数转换器
│ │ ├── ResultSetToObjectConverter.java # ResultSet 转对象转换器
│ │ └── TypeHandler.java # 类型处理器
│ ├── pagination/
│ │ ├── PaginationHandler.java # 分页处理器
│ │ ├── PageResult.java # 分页结果封装
│ │ ├── PaginationStrategy.java # 分页策略接口
│ │ ├── MySQLPaginationStrategy.java # MySQL 分页策略
│ │ ├── OraclePaginationStrategy.java # Oracle 分页策略
│ │ └── H2PaginationStrategy.java # H2 分页策略
│ ├── batch/
│ │ ├── BatchOperationHandler.java # 批量操作处理器
│ │ └── BatchResult.java # 批量操作结果
│ ├── logger/
│ │ └── DatabaseLogger.java # 数据库日志核心
│ ├── util/
│ │ └── DatabaseUtil.java # 静态工具类(主入口)
│ └── autoconfigure/
│ └── DatabaseAutoConfiguration.java # Spring Boot 自动配置
├── src/main/resources/
│ ├── META-INF/
│ │ └── spring.factories # Spring Boot 自动配置声明
│ ├── sql/ # SQL XML 文件目录
│ └── database-default.yml # 默认配置模板
└── pom.xml
02.常用信息
a.总体架构解析
通过一系列静态内部类,
将数据库操作的不同职责(数据源管理、SQL解析、对象映射、SQL执行、分页、批处理)进行模块化隔离,
并对外提供统一的、链式调用的静态方法入口(DatabaseUtil.accessor())。
它本质上是一个不依赖任何Spring容器的、纯粹的JDBC封装库,但它巧妙地借鉴了MyBatis的诸多核心理念,尤其是XML动态SQL解析。
b.数据源管理(DataSourceCore)
a.实现方式
a.配置
通过一个静态二维数组DATABASE_MANAGE_CONNECTIONS硬编码数据库连接信息,而不是使用外部配置文件(如application.yml)。
b.连接池
内置了对Druid连接池的支持,也可以回退到自定义的SimpleDataSource(一个简单的JDBCDriverManager封装)。
c.多数据源
通过一个ConcurrentHashMap<String, DataSource>来管理多个数据源。它实现了非常精巧的线程级数据源切换机制,
利用ThreadLocal(THREAD_DATASOURCE_CONTEXT)允许在单个线程中临时指定使用哪个数据源,
这类似于dynamic-datasource-spring-boot-starter的核心思想。
d.并发控制
使用了StampedLock这种比ReentrantReadWriteLock更高级的读写锁来保护数据源的注册和获取,显示出作者对并发编程有很深的理解。
b.与MyBatis-Plus的对比
a.MP
不负责数据源管理。它遵循“控制反转”(IoC)原则,将数据源的创建和管理完全委托给Spring容器。
我们只需要在Spring中配置一个DataSourceBean(通常通过spring-boot-starter-jdbc和HikariCP或Druid的starter自动配置),MP就会自动使用它。
优点是专业分工、高度解耦,充分利用了Spring生态的成熟能力,配置灵活(yml/properties),切换数据源实现(Hikari/Druid)成本极低。
缺点是强依赖Spring容器。
b.DatabaseUtil
优点是独立、自洽,不依赖Spring,可以在任何Java项目中即插即用,比如一个简单的命令行工具。
缺点是配置僵化、重复造轮子,且其多数据源管理方案虽巧妙,但成熟度和功能性远不如专业的动态数据源框架。
c.动态SQL解析(MyBatisSqlParser)
a.说明
这是整个工具类技术含量最高、最核心的部分,它几乎完整地复刻了MyBatisXML动态SQL的解析逻辑
b.实现方式
a.标签解析
它没有使用标准的XML解析器(如DOM或SAX),
而是通过大量正则表达式(Pattern, Matcher)来逐个匹配和替换XML中的动态标签,如<if>,<where>,<foreach>,<include>等。
b.表达式求值
对于test="..."这样的条件表达式,它引入了Apache Commons JEXL库来动态计算表达式的值。
这是一个非常聪明的选择,JEXL是一个轻量级且功能强大的表达式语言引擎。
c.参数处理
它能正确解析#{...}占位符,将其替换为JDBC的?,并按顺序提取出参数值列表。同时,它也支持${...}的直接字符串替换。
d.递归与缓存
通过do-while循环和递归调用processDynamicTags来处理复杂的嵌套标签,并设计了SQL片段缓存(<sql>标签)和文件级缓存。
c.与MyBatis-Plus的对比
a.MP,底层是MyBatis
使用OGNL(Object-Graph Navigation Language)作为默认的表达式求值引擎,并拥有一套非常成熟、健壮的XML解析模型。
MyBatis在启动时会将所有XML解析成MappedStatement对象,这是一个包含了所有SQL执行信息的结构化对象,而不仅仅是字符串。
完胜。MyBatis的动态SQL解析是其核心竞争力之一,其设计更加严谨、性能更高(预解析)、功能更全面、扩展性更好。
b.DatabaseUtil
优点是实现了核心功能,并且不依赖MyBatis庞大的体系。
缺点是健壮性存疑。完全基于正则表达式来解析嵌套的XML结构是非常脆弱的,一旦XML格式稍有不规范(如多余的空格、换行),就可能导致解析失败。
而MyBatis的解析器经过了千锤百炼,能处理各种复杂的边界情况。
d.对象与结果集映射(ObjectMapper)
a.说明
这个模块负责“入参”和“出参”的转换,即Java对象和数据库记录之间的映射。
b.它的实现方式
a.入参(ObjectToParameterConverter)
将传入的Java对象(POJO、Map、基本类型)转换为一个Map<String, Object>,这个Map后续会作为JEXL表达式求值的上下文。
这里它巧妙地使用了Jackson库(ObjectMapper.convertValue)来完成POJO到Map的转换,这是一个高效且省事的做法。
b.出参(ResultSetToObjectConverter)
这是另一大亮点。它不采用传统的JDBCResultSet->POJO的反射赋值方式,
而是走了另一条路:ResultSet->Map<String, Object>->Jackson->强类型POJO。
c.智能映射
为了解决数据库列名(如user_name)与Java属性名(如userName)的映射问题,它实现了非常智能的“多重尝试”机制:
1.先用默认的Jackson(驼峰命名)尝试转换。
2.如果失败,再用配置了SNAKE_CASE策略的Jackson尝试转换。
3.如果还失败,就进入一个convertWithSmartFieldMapping方法,进行各种命名变体(驼峰转下划线、全大写、全小写等)的暴力匹配。
b.与MyBatis-Plus的对比
a.MP
直接通过ResultMap或默认的驼峰-下划线映射规则,
在遍历ResultSet时,通过反射(带有高效缓存)直接将列值赋给POJO的字段。它避免了创建中间Map对象的开销。
性能更高、更直接。虽然默认的映射规则不如DatabaseUtil的“智能匹配”那么“暴力”,
但通过@TableField注解或ResultMap可以精确控制任何复杂的映射关系,更加稳定可控。
b.DatabaseUtil
优点是非常灵活和宽容,对于不规范的数据库列名有很强的适应性。充分利用了Jackson的强大能力。
缺点是性能较低,ResultSet->Map->Object的两次转换,以及为每一行都创建一个Map,会带来额外的内存和CPU开销,尤其是在处理大量数据时。
e.API与执行器(DatabaseAccessor, MyBatisSqlExecutor)
a.说明
这部分是暴露给开发者的接口,以及实际执行JDBC操作的代码。
b.实现方式
a.链式API
DatabaseAccessor类提供了一个优雅的链式API(Fluent Interface),
例如accessor("db1").setTransaction().selectListByMp(...)。这让调用代码非常流畅。
b.事务管理
提供了手动的、程序化的事务管理(setTransaction, commit, rollback)。
事务的Connection对象被保存在DatabaseAccessor实例中。
c.功能全面
除了基本的增删改查,还内置了分页(PaginationHandler)和批量操作(BatchOperationHandler)的完整实现。
分页能自动识别MySQL、Oracle、H2等数据库并应用不同的SQL方言,批量操作甚至实现了跨数据库的UPSERT(合并/更新插入)逻辑。
b.与MyBatis-Plus的对比
a.MP
a.API
提供的是基于Mapper接口的声明式API。开发者只需定义接口方法,无需实现。
这是MP(以及MyBatis)最核心的优势之一,代码极其简洁。
b.事务
使用Spring的声明式事务(@Transactional),
通过AOP实现,对业务代码无侵入,功能强大且不易出错。
c.分页/批处理
分页通过插件(PaginationInterceptor)实现,对业务代码透明。
批量插入通过MP内置的saveBatch方法实现,简单高效。
b.优势对比
a.DatabaseUtil
链式API写起来很舒服,功能高度集成。但手动事务管理是其最大弱点,容易忘记提交/回滚,导致资源泄露或数据不一致。
b.MP
全面超越。声明式API、声明式事务、插件化架构,都代表了更现代、更高效、更低耦合的设计思想。
f.总结与最终评价
a.说明
特性 DatabaseUtil 实现 MyBatis-Plus / Spring 生态系统 评价
设计哲学 一体化、自包含的工具集 (Toolkit) 解耦、可插拔的框架 (Framework) MP的设计更符合现代软件工程原则。
依赖管理 零Spring依赖,非常独立 强依赖Spring IoC容器 DatabaseUtil适用于无Spring的轻量环境。
配置方式 Java代码硬编码 外部化配置文件 (application.yml) MP配置极其灵活,适应不同环境。
SQL解析 正则表达式 + JEXL引擎 专业的XML解析器 + OGNL引擎 MyBatis的解析器更健壮、性能更高。
对象映射 ResultSet -> Map -> Jackson -> Object ResultSet -> 反射 -> Object DatabaseUtil更灵活宽容,但MP性能更好。
API 链式调用 (Fluent API) 声明式接口 (Mapper Interface) MP的Mapper接口更简洁,开发效率更高。
事务管理 手动、程序化事务 AOP声明式事务 (@Transactional) Spring的声明式事务完胜,更安全、无侵入。
扩展性 较低,需修改源码 极高,通过插件(Interceptor)机制 MP的插件机制是其强大生命力的保证。
b.最终评价
这个 DatabaseUtil 是一个令人印象深刻的技术作品,
它展示了作者深厚的Java功底、对JDBC的精通以及对数据库框架核心原理的深刻理解。
它几乎是一个人实现了一个微缩版的MyBatis。
-------------------------------------------------------------------------------------------------
然而,从工程实践的角度来看,它是一个典型的“重复造轮子”的例子。
虽然实现了很多高级功能,但在健壮性、性能、可维护性和生态系统方面,与身经百战的MyBatis-Plus + Spring全家桶相比,仍然存在巨大差距。
-------------------------------------------------------------------------------------------------
结论:在任何正式的、中大型的企业级项目中,毫无疑问应该选择 MyBatis-Plus。
而这个 DatabaseUtil 则可以作为一个极佳的学习案例,用来深入理解ORM框架底层的运作机制。
03.常用信息
a.核心API示例
// 1. 注册数据源(启动时自动注册)
DatabaseConfig config = new DatabaseConfig(
"jdbc:mysql://localhost:3306/test",
"root",
"password",
"com.mysql.cj.jdbc.Driver"
);
DataSourceCore.register("main_db", config);
DataSourceCore.setDefaultDataSource("main_db");
// 2. 简单查询(静态 API)
List<User> users = DatabaseUtil.selectList(
"SELECT * FROM users WHERE age > ?",
User.class,
18
);
// 3. XML SQL 查询
// 文件:src/main/resources/sql/user.xml
// <sql id="findByAge">
// SELECT * FROM users WHERE age > #{age}
// </sql>
List<User> users = DatabaseUtil.executeXmlQuery(
"user.findByAge",
User.class,
Map.of("age", 18)
);
// 4. 动态 SQL(MyBatis 风格)
// <select id="findUsers">
// SELECT * FROM users
// <where>
// <if test="name != null">
// AND name LIKE CONCAT('%', #{name}, '%')
// </if>
// <if test="age != null">
// AND age > #{age}
// </if>
// </where>
// </select>
Map<String, Object> params = new HashMap<>();
params.put("name", "John");
params.put("age", 18);
List<User> users = DatabaseUtil.executeXmlQuery(
"user.findUsers",
User.class,
params
);
// 5. 事务操作(链式 API)
try (DatabaseAccessor accessor = DatabaseUtil.accessor()) {
accessor.setTransaction();
accessor.execute(
"INSERT INTO users (name, age) VALUES (?, ?)",
"John", 25
);
accessor.execute(
"UPDATE accounts SET balance = balance - ? WHERE user_id = ?",
100, 1
);
accessor.commit();
} catch (Exception e) {
// 自动回滚
throw e;
}
// 6. 批量操作
List<Object[]> batchParams = Arrays.asList(
new Object[]{"User1", 20},
new Object[]{"User2", 30},
new Object[]{"User3", 40}
);
int[] results = DatabaseUtil.executeBatch(
"INSERT INTO users (name, age) VALUES (?, ?)",
batchParams
);
// 7. 分页查询
PageResult<User> page = DatabaseUtil.selectPage(
"SELECT * FROM users WHERE age > ?",
User.class,
1, // 页码
10, // 每页大小
18 // 查询参数
);
System.out.println("总记录数: " + page.getTotal());
System.out.println("总页数: " + page.getPages());
List<User> users = page.getRecords();
// 8. 链式访问器(推荐用于复杂操作)
try (DatabaseAccessor accessor = DatabaseUtil.accessor("slave_db")) {
// 查询
List<User> users = accessor.selectList(
"SELECT * FROM users WHERE status = ?",
User.class,
1
);
// 插入
User newUser = new User("John", 25);
int rows = accessor.insert(
"INSERT INTO users (name, age) VALUES (?, ?)",
newUser.getName(),
newUser.getAge()
);
// 更新
accessor.update(
"UPDATE users SET age = ? WHERE id = ?",
26, 1
);
// 删除
accessor.delete("DELETE FROM users WHERE id = ?", 1);
}
// 9. 对象映射插入
User user = new User();
user.setName("John");
user.setAge(25);
user.setEmail("[email protected]");
DatabaseUtil.insert(
"INSERT INTO users (name, age, email) VALUES (?, ?, ?)",
user
);
// 10. 线程级数据源切换
try {
DataSourceCore.useThreadDataSource("report_db");
List<Report> reports = DatabaseUtil.selectList(
"SELECT * FROM reports",
Report.class
);
} finally {
DataSourceCore.clearThreadDataSource();
}
b.依赖
<dependencies>
<!-- Druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Jackson JSON 处理(对象映射)-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- JSQLParser(SQL 解析)-->
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</dependency>
<!-- Apache Commons JEXL(表达式引擎)-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jexl3</artifactId>
</dependency>
<!-- SLF4J 日志接口 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- Spring Boot Starter(可选)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
c.核心设计特点
a.双数据源模式
Druid:生产环境推荐,功能完整(连接池、监控、SQL 防火墙)
SimpleDataSource:测试环境或轻量级场景,零配置快速启动
全局开关:USE_DRUID_DATASOURCE
线程级开关:THREAD_DRUID_ENABLED(ThreadLocal)
b.MyBatis 风格 XML
支持标签:<if>、<where>、<set>、<foreach>、<choose>、<trim>
参数语法:#{param}、${param}
自动扫描目录:src/main/resources/sql/、src/main/resources/mapper/
SQL ID 格式:文件名.sqlId(如:user.findByAge)
c.智能对象映射
驼峰命名自动转换(userName <-> user_name)
多策略尝试(驼峰优先,下划线兜底)
嵌套对象支持
集合类型支持(List、Set、Map)
类型缓存机制(提升性能)
d.分页策略
自动检测数据库类型(通过 JDBC URL)
MySQL:LIMIT offset, size
Oracle:ROW_NUMBER() OVER / WITH 子句
H2:LIMIT size OFFSET offset
PostgreSQL:LIMIT size OFFSET offset
e.SQL 日志
DEBUG_LOG_ENABLED:普通日志开关
SQL_DEBUG_LOG_ENABLED:SQL 执行日志开关
完整 SQL 输出(参数已填充)
参数类型和值详情
执行耗时统计
f.事务管理
链式 API:setTransaction() -> 操作 -> commit()/rollback()
异常自动回滚
支持 try-with-resources(AutoCloseable)
嵌套事务检测
d.动态 SQL 标签支持
<if test="condition"> - 条件判断
<where> - 自动添加 WHERE 并处理 AND/OR
<set> - 自动添加 SET 并处理逗号
<foreach> - 集合遍历(IN 查询)
<choose><when><otherwise> - 多分支选择
<trim> - 前后缀处理
<bind> - 变量绑定
e.批量操作支持
批量插入 - executeBatch()
批量更新 - executeBatch()
批量删除 - executeBatch()
批量 UPSERT - executeBatchUpsert()(MySQL ON DUPLICATE KEY UPDATE)
f.最佳实践
a.使用 try-with-resources
try (DatabaseAccessor accessor = DatabaseUtil.accessor()) {
accessor.setTransaction();
// 数据库操作
accessor.commit();
} // 自动关闭连接
b.线程级数据源切换后必须清除
try {
DataSourceCore.useThreadDataSource("temp_db");
// 操作
} finally {
DataSourceCore.clearThreadDataSource(); // 避免线程池污染
}
c.开启 SQL 日志调试
LoggerCore.setDebugLogEnabled(true);
LoggerCore.setSqlDebugLogEnabled(true);
d.分页查询优化
// 使用 COUNT(*) 优化(避免扫描大表)
PageResult<User> page = DatabaseUtil.selectPage(
"SELECT * FROM users WHERE status = 1",
"SELECT COUNT(*) FROM users WHERE status = 1", // 自定义 count 查询
User.class,
1,
10
);
e.XML SQL 组织
// 按模块组织 XML 文件
src/main/resources/sql/
├── user.xml # 用户相关 SQL
├── order.xml # 订单相关 SQL
└── report.xml # 报表相关 SQL
g.使用场景
中小型项目数据库访问层
无 MyBatis 依赖的项目
快速原型开发
测试工具类数据库操作
数据迁移脚本
报表生成工具
4.4 RedissonUtil
01.常见信息
a.功能
多 Redis 数据源动态管理(Lettuce + Redisson 双引擎)
懒加载连接池机制(首次访问时初始化)
线程级数据源切换(ThreadLocal 上下文)
完整的 Redis 数据类型操作(String、Hash、List、Set、ZSet)
Redis 事务支持(MULTI/EXEC/DISCARD)
乐观锁支持(WATCH/UNWATCH)
分布式锁(可重入锁、公平锁、读写锁)
分布式同步工具(信号量、倒计时器)
发布/订阅消息系统
Lua 脚本执行
地理空间操作(GEO)
Redis Stream 支持
HyperLogLog 基数统计
布隆过滤器
位图操作
b.源文件
pisces-boot-app/src/main/java/cn/myslayers/app/utils/
└── RedissonUtil.java → 企业级 Redis 完整工具类(6900+ 行)
c.目标结构
libra-spring-boot-starter-redis/
├── src/main/java/cn/myslayers/libra/framework/redis/
│ ├── core/
│ │ ├── RedisDataSourceManager.java # Redis 数据源管理器
│ │ ├── LettuceConnectionFactory.java # Lettuce 连接池工厂
│ │ ├── RedissonClientFactory.java # Redisson 客户端工厂
│ │ └── RedisDataSourceConfig.java # Redis 数据源配置类
│ ├── accessor/
│ │ ├── RedisAccessor.java # Redis 链式访问器(支持事务)
│ │ └── RedisTransactionManager.java # 事务管理器
│ ├── ops/
│ │ ├── KeyOps.java # 键操作
│ │ ├── StringOps.java # 字符串操作
│ │ ├── HashOps.java # 哈希操作
│ │ ├── ListOps.java # 列表操作
│ │ ├── SetOps.java # 集合操作
│ │ ├── ZSetOps.java # 有序集合操作
│ │ ├── BitmapOps.java # 位图操作
│ │ ├── GeoOps.java # 地理空间操作
│ │ ├── StreamOps.java # Stream 操作
│ │ ├── HyperLogLogOps.java # HyperLogLog 操作
│ │ ├── BloomFilterOps.java # 布隆过滤器操作
│ │ └── PubSubOps.java # 发布订阅操作
│ ├── lock/
│ │ ├── ReentrantLockOps.java # 可重入锁
│ │ ├── FairLockOps.java # 公平锁
│ │ └── ReadWriteLockOps.java # 读写锁
│ ├── sync/
│ │ ├── SemaphoreOps.java # 信号量
│ │ └── CountDownLatchOps.java # 倒计时器
│ ├── script/
│ │ └── ScriptOps.java # Lua 脚本执行器
│ ├── util/
│ │ ├── RedisUtil.java # 静态工具类(主入口)
│ │ └── LoggerCore.java # 日志核心
│ ├── annotation/
│ │ ├── @RedisDataSource.java # 数据源注解
│ │ └── @RedisTransactional.java # 事务注解
│ └── autoconfigure/
│ └── RedisAutoConfiguration.java # Spring Boot 自动配置
├── src/main/resources/
│ ├── META-INF/
│ │ └── spring.factories # Spring Boot 自动配置声明
│ └── redis-default.yml # 默认配置模板
└── pom.xml
02.常用信息
a.核心API示例
// 1. 注册数据源(程序启动时自动注册)
RedisDataCore.register("cache_db", "127.0.0.1", 6379, "", 0, 8, 8, 0);
RedisDataCore.setDefaultKey("cache_db");
// 2. 简单操作(静态 API)
RedisUtil.set("user:1001", "John");
String name = RedisUtil.get("user:1001");
RedisUtil.expire("user:1001", 3600, TimeUnit.SECONDS);
// 3. 链式访问器(推荐用于复杂操作)
try (RedisAccessor accessor = RedisUtil.accessor()) {
accessor.StringOps.set("key1", "value1");
accessor.HashOps.hset("user:1001", "name", "John");
List<String> list = accessor.ListOps.lrange("mylist", 0, -1);
}
// 4. 事务操作(完整支持 MULTI/EXEC/DISCARD)
try (RedisAccessor accessor = RedisUtil.accessor()) {
accessor.watch("balance:1001"); // 乐观锁
accessor.multi();
accessor.StringOps.incrBy("balance:1001", -100);
accessor.StringOps.incrBy("balance:1002", 100);
List<Object> results = accessor.exec();
if (results == null) {
System.out.println("事务失败:被其他操作修改");
}
}
// 5. 分布式锁
RedisUtil.lock("order:lock:123", 30, TimeUnit.SECONDS);
try {
// 业务逻辑
} finally {
RedisUtil.unlock("order:lock:123");
}
// 6. 线程级数据源切换
try {
LettuceCore.use("cache_db");
RedisUtil.set("temp_key", "temp_value");
} finally {
LettuceCore.clear(); // 必须清除,避免线程池污染
}
// 7. 发布/订阅
RedisUtil.subscribe("news_channel", (channel, msg) -> {
System.out.println("收到消息: " + msg);
});
RedisUtil.publish("news_channel", "Hello World");
// 8. 地理空间操作
RedisUtil.geoAdd("cities", 116.4074, 39.9042, "Beijing");
RedisUtil.geoAdd("cities", 121.4737, 31.2304, "Shanghai");
Double distance = RedisUtil.geoDistance("cities", "Beijing", "Shanghai", GeoUnit.KILOMETERS);
b.依赖
<dependencies>
<!-- Lettuce 客户端(连接池 + 基础命令)-->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
<!-- Redisson 客户端(分布式锁 + 高级功能)-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
<!-- Apache Commons Pool2(连接池)-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- SLF4J 日志接口 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- Spring Boot Starter(可选)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
c.核心设计特点
a.双引擎架构
Lettuce:基础命令、连接池管理、事务支持
Redisson:分布式锁、同步工具、高级数据结构
b.懒加载机制
连接池仅在首次使用时初始化,提升应用启动速度
采用双重检查锁定(DCL)确保线程安全
c.多数据源支持
全局默认数据源(AtomicReference 保证线程安全)
线程级临时数据源(ThreadLocal 实现线程隔离)
优先级:线程级设置 > 全局默认设置
d.资源管理
RedisAccessor 实现 AutoCloseable,支持 try-with-resources
自动归还连接到连接池,防止连接泄漏
事务未完成时自动执行 DISCARD
e.日志调试
全局调试开关(DEBUG_LOG_ENABLED)
支持动态开启/关闭日志输出
详细记录数据源切换、连接获取、事务状态
d.数据类型支持
String - 字符串操作(set, get, incr, append 等)
Hash - 哈希表操作(hset, hget, hincrby 等)
List - 列表操作(lpush, rpush, lrange 等)
Set - 集合操作(sadd, smembers, sinter 等)
ZSet - 有序集合操作(zadd, zrange, zrank 等)
Bitmap - 位图操作(setbit, getbit, bitcount 等)
Geo - 地理空间(geoadd, georadius 等)
Stream - 流数据结构(xadd, xread 等)
HyperLogLog - 基数统计(pfadd, pfcount 等)
BloomFilter - 布隆过滤器(add, contains 等)
e.分布式功能
可重入锁 - 支持同一线程多次获取同一把锁
公平锁 - 按照请求顺序分配锁,避免饥饿
读写锁 - 读锁共享,写锁互斥
信号量 - 限制并发访问数量
倒计时器 - 等待多个任务完成
发布/订阅 - 消息广播机制
Lua 脚本 - 原子性复杂操作
f.最佳实践
a.使用 try-with-resources
try (RedisAccessor accessor = RedisUtil.accessor()) {
// Redis 操作
} // 自动释放连接
b.线程级数据源切换后必须清除
try {
LettuceCore.use("temp_db");
// 操作
} finally {
LettuceCore.clear(); // 避免线程池污染
}
c.分布式锁使用模板
boolean locked = false;
try {
locked = RedisUtil.tryLock("lock_key", 10, 30, TimeUnit.SECONDS);
if (locked) {
// 业务逻辑
}
} finally {
if (locked) {
RedisUtil.unlock("lock_key");
}
}
d.事务 + 乐观锁
try (RedisAccessor accessor = RedisUtil.accessor()) {
accessor.watch("important_key"); // 监视键
String value = accessor.StringOps.get("important_key");
accessor.multi();
// 基于 value 的业务逻辑
accessor.StringOps.set("important_key", newValue);
List<Object> results = accessor.exec();
if (results == null) {
// 重试或报错处理
}
}
4.5 PostmanUtil
01.常见信息
a.功能
嵌入式 HTTP 服务器(基于 Java 内置 HttpServer)
注解驱动的路由系统(@PostmanController + @PostmanMapping)
自动包扫描和 Controller 注册
统一响应格式(Result<T>)
可插拔的认证策略(Bearer Token、HMAC 签名)
请求详情追踪(自动生成 curl 命令)
灵活的日志系统(调试日志、启动日志、请求日志独立控制)
线程级请求上下文(RequestContextHolder)
手动路由注册支持
源文件行号追踪(方便调试)
b.源文件
pisces-boot-app/src/main/java/cn/myslayers/app/utils/
└── PostmanUtil.java → 轻量级 API 测试工具(915 行)
c.目标结构
libra-spring-boot-starter-postman/
├── src/main/java/cn/myslayers/libra/framework/postman/
│ ├── core/
│ │ ├── PostmanServer.java # HTTP 服务器核心
│ │ ├── RouteRegistry.java # 路由注册表
│ │ └── RequestContextHolder.java # 请求上下文管理器
│ ├── handler/
│ │ ├── RouteHandler.java # 路由处理器
│ │ ├── HandlerMethod.java # 方法处理器封装
│ │ └── HandlerScanner.java # 控制器扫描器
│ ├── auth/
│ │ ├── AuthManager.java # 认证管理器
│ │ ├── AuthStrategy.java # 认证策略接口
│ │ ├── BearerTokenStrategy.java # Bearer Token 认证
│ │ └── SignatureStrategy.java # HMAC 签名认证
│ ├── annotation/
│ │ ├── @PostmanController.java # 控制器注解
│ │ ├── @PostmanMapping.java # 路由映射注解
│ │ └── @RequiresAuth.java # 认证注解
│ ├── response/
│ │ └── Result.java # 统一响应格式
│ ├── logger/
│ │ └── PostmanLogger.java # 日志核心
│ ├── util/
│ │ └── PostmanUtil.java # 静态工具类(主入口)
│ └── autoconfigure/
│ └── PostmanAutoConfiguration.java # Spring Boot 自动配置
├── src/main/resources/
│ ├── META-INF/
│ │ └── spring.factories # Spring Boot 自动配置声明
│ └── postman-default.yml # 默认配置模板
└── pom.xml
02.常用信息
a.核心API示例
// 1. 快速启动服务器(全局模式)
public static void main(String[] args) {
PostmanUtil.setPort(5176);
PostmanUtil.setDebugLogEnabled(true);
PostmanUtil.setStartupLogEnabled(true);
PostmanUtil.setRequestDetailLogEnabled(true);
// 启动服务器并扫描指定包
PostmanUtil.HttpCore.startServer(MyController.class);
}
// 2. 定义 Controller(注解模式)
@PostmanController("/api")
public class UserController {
@PostmanMapping(value = "/users", method = RequestMethod.GET)
public Result<List<User>> getUsers() {
List<User> users = userService.findAll();
return Result.ok(users);
}
@PostmanMapping(value = "/user", method = RequestMethod.POST)
public Result<User> createUser() {
// 获取请求体
User user = getBody(User.class);
userService.save(user);
return Result.ok(user);
}
// 带认证的接口
@PostmanMapping(value = "/user/delete", method = RequestMethod.DELETE)
@RequiresAuth(BearerTokenStrategy.class)
public Result<Void> deleteUser() {
Map<String, String> params = getParams();
String userId = params.get("id");
userService.delete(userId);
return Result.ok(null);
}
}
// 3. 手动注册路由
PostmanUtil.ManualRegistry.register("GET", "/health", (exchange) -> {
return Result.ok("Service is running");
});
// 4. 配置认证策略
// Bearer Token 认证
AuthManager.setBearerToken("my-secret-token-123");
// HMAC 签名认证
AuthManager.setSignatureSecret("my-signature-secret");
// 5. 自定义认证策略
public class CustomAuthStrategy implements AuthManager.Strategy {
@Override
public boolean authenticate(HttpExchange exchange) {
String apiKey = exchange.getRequestHeaders().getFirst("X-API-Key");
return "valid-api-key".equals(apiKey);
}
}
// 6. 使用请求上下文
@PostmanMapping(value = "/echo", method = RequestMethod.POST)
public Result<Map<String, Object>> echo() {
// 获取原始 HttpExchange
HttpExchange exchange = RequestContextHolder.getHttpExchange();
// 获取查询参数
Map<String, String> params = RequestContextHolder.getQueryParams();
// 获取请求体
String body = RequestContextHolder.getRequestBody();
Map<String, Object> response = new HashMap<>();
response.put("headers", exchange.getRequestHeaders());
response.put("params", params);
response.put("body", body);
return Result.ok(response);
}
// 7. 停止服务器
PostmanUtil.HttpCore.stopServer();
b.依赖
<dependencies>
<!-- Java 内置 HttpServer(JDK 自带,无需额外依赖)-->
<!-- Jackson JSON 处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Fastjson(可选,用于请求体解析)-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!-- Reflections(注解扫描)-->
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
</dependency>
<!-- SLF4J 日志接口 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- Spring Boot Starter(可选)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
c.核心设计特点
a.轻量级架构
基于 JDK 内置 HttpServer,无需 Tomcat/Jetty
单文件实现,零配置快速启动
线程池处理请求(默认 10 个工作线程)
b.注解驱动
@PostmanController:标记控制器类,指定基础路径
@PostmanMapping:标记接口方法,指定路径和 HTTP 方法
@RequiresAuth:标记需要认证的接口,支持多策略组合
c.灵活的日志系统
DEBUG_LOG_ENABLED:核心调试日志(服务器启动、扫描等)
STARTUP_LOG_ENABLED:启动时打印已注册接口列表
REQUEST_DETAIL_LOG_ENABLED:请求详情日志(curl、入参、出参、耗时)
d.认证策略
Strategy 接口:可扩展的认证策略
BearerTokenStrategy:标准 Bearer Token 认证
SignatureStrategy:HMAC-SHA256 签名认证(防重放)
支持多策略组合(所有策略通过才认证成功)
e.请求追踪
自动生成等效 curl 命令
记录请求耗时
详细的入参、出参日志
f.端口管理
启动前自动检测并关闭占用端口的进程
跨平台支持(Windows、macOS、Linux)
防止端口冲突
d.HTTP 方法支持
GET - 查询操作(参数通过 Query String 传递)
POST - 创建操作(参数通过 Request Body 传递)
PUT - 更新操作(参数通过 Request Body 传递)
DELETE - 删除操作(参数通过 Query String 传递)
PATCH - 部分更新(参数通过 Request Body 传递)
OPTIONS - 跨域预检请求
e.认证策略详解
a.Bearer Token 认证
客户端请求头:Authorization: Bearer <token>
服务端验证:提取 Bearer 后的 token 与预设 token 比对
b.HMAC 签名认证
a.客户端请求头
X-Timestamp: 1672531200000 # Unix 时间戳(毫秒)
X-Nonce: random-string-123 # 随机字符串(防重放)
X-Api-Signature: <base64-signature> # HMAC-SHA256 签名
b.签名生成规则
stringToSign = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE
signature = Base64(HMAC-SHA256(stringToSign, secret))
c.服务端验证
1.检查时间戳是否在有效窗口内(默认 5 分钟)
2.使用相同算法生成签名并比对
f.最佳实践
a.Controller 组织
// 使用基础路径避免重复
@PostmanController("/api/v1")
public class ApiController {
@PostmanMapping(value = "/users", method = RequestMethod.GET)
public Result<List<User>> getUsers() { ... }
}
// 实际路径:GET /api/v1/users
b.统一响应格式
// 成功响应
return Result.ok(data);
// 失败响应
return Result.error(400, "参数错误");
c.异常处理
try {
// 业务逻辑
return Result.ok(data);
} catch (Exception e) {
LoggerCore.log("ERROR", "操作失败: " + e.getMessage());
return Result.error(500, "服务器内部错误");
}
d.资源清理
// JVM 退出前停止服务器
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
PostmanUtil.HttpCore.stopServer();
}))
g.使用场景
快速 API 原型开发
单元测试模拟服务器
微服务集成测试
独立工具类 API 服务
临时测试接口
Mock 服务器
5 管理平台
5.1 项目配置
01.模块文件
a.gradle
a.wrapper
a.gradle-wrapper.jar
META-INF
org.gradle
gradle-cli-classpath.properties
gradle-cli-parameter-names.properties
gradle-wrapper-classpath.properties
gradle-wrapper-parameter-names.properties
b.gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
b.单文件
build.gradle:每个模块的核心构建文件,它包含了具体的构建逻辑和配置
gradlew:执行Gradle构建的入口脚本,它确保你使用的是正确的Gradle版本
gradlew.bat:执行Gradle构建的入口脚本,它确保你使用的是正确的Gradle版本
settings.gradle:定义了项目的整体结构,告诉Gradle有哪些模块
b.build文件夹
a.libs
a.erp-1.0-SNAPSHOT.jar
META-INF
MANIFEST.MF:Manifest-Version: 1.0
b.erp-1.0-SNAPSHOT.war
META-INF
MMANIFEST.MF:Manifest-Version: 1.0
b.tmp
a.jar
MANIFEST.MF 内容为 Manifest-Version: 1.0
b.war
MANIFEST.MF 内容为 Manifest-Version: 1.0
c.gradle文件夹
a.wrapper
a.gradle-wrapper.jar
META-INF
org.gradle
gradle-cli-classpath.properties
gradle-cli-parameter-names.properties
gradle-wrapper-classpath.properties
gradle-wrapper-parameter-names.properties
b.gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
02.模块分析
a.组成
a.关系
settings.gradle 定义了项目的整体结构,告诉Gradle有哪些模块。
gradlew (或 gradlew.bat) 是执行Gradle构建的入口脚本,它确保你使用的是正确的Gradle版本。
gradle.properties 提供了全局或项目级别的配置属性。
build.gradle 是每个模块的核心构建文件,它包含了具体的构建逻辑和配置。
b.流程
当你运行gradlew build这样的命令时,
Gradle会首先读取settings.gradle来了解项目结构,
然后根据gradle.properties中的配置,
最后执行各个模块的build.gradle中定义的构建任务。
b.细节
a.build.gradle:每个模块的核心构建文件,它包含了具体的构建逻辑和配置
作用:这是Gradle构建脚本的核心。它定义了项目的构建逻辑,包括依赖管理、任务(tasks)定义、插件应用、以及各种构建配置。
内容:plugins: 声明项目使用的Gradle插件,例如Java插件、Android插件等。
dependencies: 定义项目所需的外部库和模块依赖。
tasks: 定义自定义的构建任务,例如编译代码、打包、运行测试等。
repositories: 配置依赖的来源,比如Maven Central、JCenter等。
其他配置: 针对不同插件的特定配置,比如Java项目的sourceCompatibility、Android项目的compileSdkVersion等。
b.gradle.properties
# 设置守护进程的初始堆大小和最大堆大小
org.gradle.jvmargs=-Xms2g -Xmx4g -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# 如果你的项目特别大,可以考虑启用并行构建
org.gradle.parallel=true
# 提高缓存大小以减少内存压力
org.gradle.caching=true
-------------------------------------------------------------------------------------------------
作用:这个文件用于配置Gradle的全局属性或项目特定的属性。你可以在这里定义一些在构建过程中需要用到的变量,比如JVM参数、代理设置、或者自定义的构建属性。
内容:以键值对的形式存储属性,例如org.gradle.jvmargs=-Xmx2048m(设置JVM最大内存)或者myCustomProperty=someValue。
d.gradlew
构建脚本,用于linux/mac
-------------------------------------------------------------------------------------------------
这两个是Gradle Wrapper的脚本文件。
Gradle Wrapper允许你在没有预先安装Gradle的情况下运行Gradle构建。
它会检查本地是否安装了指定版本的Gradle,如果没有,它会自动下载并使用该版本。
内容:它们是可执行脚本,通常不需要手动修改。它们会调用Gradle Wrapper JAR文件来执行构建任务。
e.gradlew.bat
构建脚本,用于win
-------------------------------------------------------------------------------------------------
这两个是Gradle Wrapper的脚本文件。
Gradle Wrapper允许你在没有预先安装Gradle的情况下运行Gradle构建。
它会检查本地是否安装了指定版本的Gradle,如果没有,它会自动下载并使用该版本。
内容:它们是可执行脚本,通常不需要手动修改。它们会调用Gradle Wrapper JAR文件来执行构建任务。
f.index.html
<html>
<meta http-equiv="refresh" content="0; url=/erp/ds/jsp/dsjjNeweip.jsp">
</html>
-------------------------------------------------------------------------------------------------
整个项目首页,对应【测试环境】
g.settings.gradle
rootProject.name = 'erp'
include 'il'
include 'sb'
-------------------------------------------------------------------------------------------------
作用: 这个文件是Gradle构建的入口,它定义了项目中包含的所有子项目(模块)。
当你有一个多模块项目时,你需要在settings.gradle中声明这些模块。
内容: 通常包含include 'module1', 'module2'这样的语句,用于告诉Gradle你的项目由哪些模块组成。对于单模块项目,这个文件可能非常简单,甚至只有一行代码。
5.2 前端管理
01.常用信息1
a.说明
a.本机
v14.16.1
v20.10.0
-------------------------------------------------------------------------------------------------
nvm alias default v14.16.1
nvm alias default v20.10.0
b.分支
kh4j-ui(默认master) 14.21.1
kh4j-ui-performance 14.21.1
b.npm
a.镜像
npm set registry http://172.17.8.54/
npm adduser --registry http://172.17.8.54/
npm profile set password --registry http://172.17.8.54/
-------------------------------------------------------------------------------------------------
npm login
j057240
qwER159263
-------------------------------------------------------------------------------------------------
npm config ls
b.命令
# 安装
npm install
# 启动
npm run dev
c.pnpm
a.镜像
pnpm set registry http://172.17.8.54/
pnpm adduser --registry http://172.17.8.54/
pnpm profile set password --registry http://172.17.8.54/
-------------------------------------------------------------------------------------------------
pnpm login
j057240
qwER159263
-------------------------------------------------------------------------------------------------
pnpm c get
b.命令
# 安装
pnpm install
# 以 dev 模式启动 Vite 开发服务器
pnpm dev
# 如果你使用的是 npm
npm run dev
02.常用信息2
a.附件上传
a.问题
图片上传
GET请求,/view/**,正则匹配不一样
绕开网关可以访问,走网关不可以访问
b.SpringMvc 使用 AntPathMatcher 模式
/view/** 可以匹配 /view/ 下任意层级路径,包括多级目录和带特殊字符的文件名
* 匹配单层路径部分,** 可跨多层目录
特殊字符或中文路径一般可直接匹配,只要 Controller 方法对应
c.SpringGateway 使用断言(Predicates)和 RegExp,且处理方式更严格:
/view/** 理论上能匹配多级目录。但 Gateway 配置时还涉及 StripPrefix、RewritePath 等 Filter,如果 rewrite 或 strip 配置不合理,会造成路径解析出错,比如中文/特殊符号未 decode,或路径”提前截断”。
默认断言 Path 只按 URI 匹配,若有 RewritePath,需保证正则 `(?<segment>.*)` 能处理带编码的路径,否则特殊文件名会被 Gateway 拦截/错误处理。
Gateway 处理正则时,某些符号(如 %E5%BC%A0…)需在正则和后端 Controller 都能被 decode,否则原始/转义路径难匹配,报 500
b.图片显示
a.加入白名单
位置:系统管理 -> 权限设置 -> 接口管理
筛选框(服务名称):入职管理 / 入职管理-J057117 / 入职管理-J057240
-------------------------------------------------------------------------------------------------
查看照片文件 GET /onboard/applicant/image/view/photo/** 入职管理 默认动作
白名单:是
免登白名单:是
b.前端报文
http://172.17.8.57:81/uat-api/onboard/applicant/image/view/photo/%E9%83%91%E8%89%B3_18687654321_%E7%85%A7%E7%89%87_20251014_111839.jpg
{"code":5000,"message":"请求异常,请联系管理员","data":null}
c.67服务器日志
[root@kh-zjapp ~]# ps -ef | grep java
root 1635853 1 1 9月29 ? 03:27:21 /usr/local/jdk-1.8.0_191/bin/java -jar /home/kh4j-product-onboard-service-2.3.0.jar --spring.application.name=kh4j-product-onboard
5.3 本地扩展:ext
01.快速开始
a.环境
a.正式
网址:
账户:
密码:
b.测试
网址:
账号:
密码:
b.代理
a.WiFi / 以太网
公司:172.16.10.130
阿里云:223.5.5.5
阿里云:223.6.6.6
b.surge
"icscrepo1.icsc.com.tw": "8.8.8.8#DIRECT"
fake-ip-filter:
- "icscrepo1.icsc.com.tw"
rules:
- 'DOMAIN,icscrepo1.icsc.com.tw,DIRECT'
c.clash verge
skip-proxy:追加 icscrepo1.icsc.com.tw
[Rule]:追加 DOMAIN,icscrepo1.icsc.com.tw,DIRECT
[Host]:追加 icscrepo1.icsc.com.tw = server:8.8.8.8 / icscrepo1.icsc.com.tw = 198.18.3.169(废弃) / 域名+端口 / ip+端口 / HEAD与GET请求
d.镜像
buildscript {
repositories {
maven { url 'http://icscrepo1.icsc.com.tw/repository/maven-public' }
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "gradle.plugin.com.gorylenko.gradle-git-properties:gradle-git-properties:1.5.1"
}
}
e.总结
编译,需要关闭TUN模式,以及整个Surge/Clash,需访问【icscrepo1.icsc.com.tw】构建
耗时:自动将【erp(外部源)】放入【本地外部Tomcat】对应的【webapps、work】文件夹,耗时【4分30秒】
经测试,【/Users/troyesivens/Documents/software/apache-tomcat-9.0.106】,可以完美运行
经测试,【/opt/homebrew/Cellar/tomcat@9/9.0.104/libexec】,brew安装的Tomcat,缺少配置,可自行补全
c.配置Tomcat
a.服务器
应用程序服务器:Tomcat 9.0.106
启动后默认浏览器:http://localhost:9001/erp/
虚拟机选项:-Dcom.icsc.compNo=erp -Dcom.icsc.serverName-ERPerp -Dcom.icsc.instanceNo=0l -Dcom.icsc.dpms.de.dejc300.j2ee=true
执行“更新”操作时:重启服务器
JRE:1.8
Tomcat服务器设置:HTTP端口(9001)、JMX端口(1098)
b.部署
在服务器启动时部署:erp(外部源)
应用程序上下文:/erp
c.启动
前提:必须编译整个项目
第1步:将 local/config/localAjax.xml 更新到 ../apache-tomcat-9.0.106/bin/config/erp/local/localAjax.xml
第2步:将 local/build/libs/local.jar 手动放入 WEB-INF/lib/local.jar
报错:遇到RMI,1099连接有问题,请停掉全部java应用
访问:http://localhost:9001/erp/local/ajax?_controller=localTest&_action=list
d.配置Tomcat-local
a.地址
/opt/homebrew/Cellar/tomcat@9/9.0.104/libexec/lib
文件(ojdbc8.jar)
-------------------------------------------------------------------------------------------------
/opt/homebrew/Cellar/tomcat@9/9.0.104/libexec/webapps
文件夹(erp -> META-INF -> MANIFEST.MF、war-tracker)
b.服务器
应用程序服务器:Tomcat 9.0.104
启动后默认浏览器:http://localhost:9002/erp/
虚拟机选项:-Dcom.icsc.compNo=erp -Dcom.icsc.serverName-ERPerp -Dcom.icsc.instanceNo=0l -Dcom.icsc.dpms.de.dejc300.j2ee=true
执行“更新”操作时:重启服务器
JRE:1.8
Tomcat服务器设置:HTTP端口(9002)、JMX端口(1099)
c.部署
在服务器启动时部署:erp(外部源)
应用程序上下文:/erp
d.启动
前提:必须编译整个项目
第1步:将 local/config/localAjax.xml 更新到 ../apache-tomcat-9.0.105/bin/config/erp/local/localAjax.xml
第2步:将 local/build/libs/local.jar 手动放入 WEB-INF/lib/local.jar
报错:遇到RMI,1099连接有问题,请停掉全部java应用
访问:http://localhost:9002/erp/local/ajax?_controller=localTest&_action=list
02.扩展开发
a.guide:开发规范
a.anno
@dsjcAuthResourceInfoId("HDJJE0610")
@dejcReqParam:post参数注解
@dejcTransactional:事务注解
@dejcPureSvc:返回图片
b.rules
api:对接三方,提供WS系統的api,hdjcApiWS
bsTable:连接Sql的对象
batch:批量操作API,对接rest、rpt、bsTable
-------------------------------------------------------------------------------------------------
bs:bussiness对象
di:sring中di对象
dao:dao对象、vo对象
entity:entity对象
report:report对象
helper:helper对象
type:字段类型转换
payroll:支付回调
-------------------------------------------------------------------------------------------------
rest:理解为三层中的控制层
rpt:理解为三层中的业务层,为rest的下层
-------------------------------------------------------------------------------------------------
ui:理解为三层中的controller层中返回的UI组件,返回内容为com.icsc.dpms.de.rest.dejcRestInfoOut组件,属性有msg、msgType、error、voContainer、uiCtrl、voCache、uuid
tag:理解为三层中的controller层中返回的tag组件
thrd:理解为三层中的controller层中返回的thrd组件
link:直接解析到html/jsp的代码片段,<%=hdjcLinkHR.getHRM0VOJSON()%>
upload:理解为三层中的controller层中返回的upload组件,返回内容为com.icsc.hb.upload.hbjiExcelUpload组件,属性有upload、setParameter、getObject、getMsg
-------------------------------------------------------------------------------------------------
model:直接解析到html/jsp的代码片段,<%=hbjcCommentSelectionDefault.getDefault("qry.commentSelect")%>
monthBonus:实际为 bs 中 hbjcBsAC020类 下面的一个属性,比如hbjcMonthBonus_calDeptBonusByLeader.java
-------------------------------------------------------------------------------------------------
util:工具包
b.local:本地扩展模块
a.分类1:utils工具包
a.bean
Bean2SqlUtils.java:类转sql
BeanConvertUtils.java:bean转换util
b.common
PaginationUtil:分页工具类
c.constant
MRInterfaceUrl:原料调物流接口量类
d.db
JdbcUtil.java:JDBC工具类,java:comp/env/jdbc/dserp
JdbcUtilEp.java:JDBC工具类,连接Ep数据库,java:comp/env/jdbc/dserpmq
b.分类2:utils工具包
a.file
DrjcFileUtil.java:附件工具
b.http
OkHttpUtil:HTTP工具类,支持get、post
c.json
JsonUtil:JSON工具类
d.properties
PropertiesUtil:获取资源文件工具类
e.xml
XmlParserUtils:解析XML工具类
5.4 对接权限:auth
01.Token管理体系
a.存储方式
Token 存储在 Cookie (Admin-Token)
权限列表存储在 LocalStorage (Permission)
用户信息存储在 Pinia Store (useUserStore)
b.自动注入流程
请求拦截器 → 从Cookie获取Token → 添加到请求头 → Bearer Token格式
c.核心工具
// Token操作
import { getToken, setToken, removeToken } from '@khlc/common-core/src/util/auth'
// 用户Store
import useUserStore from '@khlc/common-core/src/store/user'
const userStore = useUserStore()
02.权限控制三种方式
a.方式一: 指令式权限 (v-auth)
el-button v-auth="'featuremodelmanage_save'">保存</el-button>
b.方式二: 角色权限 (v-role)
<el-button v-role="['admin_role', 'hr_role']">管理功能</el-button>
c.方式三: 编程式权限
const canEdit = computed(() => {
return userStore.permissions.includes('system:user:edit') ||
userStore.permissions.includes('*:*:*')
})
03.请求架构三层封装
a.图示
业务组件
↓
API抽象层 (getAction, postAction)
↓
Axios拦截器 (自动添加Token, 统一错误处理)
↓
后端API
b.关键特性
自动Token注入: 拦截器自动添加 Authorization: Bearer
统一错误处理: 401自动跳转登录, 500统一提示
防抖机制: 2秒内多次401只弹一次窗
智能交互: 区分页面刷新和正常调用
04.用户信息获取方式
a.后端(Java)
// 获取当前用户编号
String userNo = AuthUtil.getLoginUserNo();
// 获取用户角色
Set<String> roles = AuthUtil.getRoles();
// 获取租户ID
String tenantId = AuthUtil.getTenantId();
b.前端(Vue)
const userStore = useUserStore()
// 获取用户信息
userStore.name // 用户姓名
userStore.userId // 用户ID
userStore.userNo // 用户工号
userStore.permissions // 权限列表
userStore.roles // 角色列表
05.数据权限控制 (后端)
a.注解式控制
@DataPermissions({
@DataPermission(
controlType = "in",
controlTableAlias = "t",
controlField = "ORG_ID",
bizType = BizOrganizationRelationEnum.PERFORMANCE_ACTIVITY
)
})
public KhPage<ActivityDTO> listActivities(KhPage<ActivityDTO> page, ActivityQuery query) {
// AOP会自动在SQL中注入组织权限过滤条件
return activityMapper.selectPage(page, query);
}
b.核心原理
超级管理员豁免: SUPER_ROLE跳过数据权限过滤
组织树过滤: 根据用户所属组织自动生成 WHERE EXISTS 条件
ThreadLocal传递: 通过 DataPermissionContextHolder 传递SQL片段
MyBatis拦截器: 在执行SQL前自动拼接权限条件
06.SSO单点登录流程
a.流程
// 1. URL携带ticket参数
http://app.com/?ticket=abc123
// 2. 通过ticket换取access_token
const authResponse = await postAction("auth/oauth/token", null, {
grant_type: "sso_rpc",
ticket: ticket,
client_code: encrypt(client_id + "," + client_secret)
})
// 3. 保存Token到Cookie
setToken(authResponse.access_token)
// 4. 获取用户详细信息
const userResponse = await getAction("/hrms/user/getByUserNo", {
userNo: authResponse.user_no
})
// 5. 设置用户信息到Store
useUserStore().setName(userResponse.data.name)
// 6. 跳转到目标页面
next({ path: redirect || "/home" })
07.关键安全机制
a.Token过期处理
// 响应拦截器捕获401
if (code === 4001 || code === 401) {
removeToken() // 清除Token
// 防抖处理,2秒内去重
FunctionExecUtil.debounceIm(() => {
// 区分页面刷新和正常调用
if (locationRefreshed || isNavigate) {
// 直接跳转登录
location.reload()
} else {
// 弹窗询问用户
ElMessageBox.confirm('登录状态已过期...', '系统提示')
}
}, "loginTimeOut", 2000)
}
b.Token自动刷新
// 每10分钟检测Token有效性
FunctionExecUtil.execInterval(() => {
envConfig().request({
url: "/auth/oauth/check_token",
params: { token: getToken() }
})
}, "autoLogin", 600000, true)
5.5 本地开发:local
01.项目创建
a.kh4j-product-onboard
a.settings.xml
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<pluginGroups></pluginGroups>
<proxies></proxies>
<servers></servers>
<mirrors>
<mirror>
<id>jingang</id>
<mirrorOf>*</mirrorOf>
<name>jingang maven</name>
<url>http://172.17.8.55/repository/maven-public</url>
</mirror>
</mirrors>
</settings>
b.安装
mvn install:install-file \
-Dfile=/path/to/your/kh4j-cloud-archetype-service-1.3.3.jar \
-DgroupId=com.khict \
-DartifactId=kh4j-cloud-archetype-service \
-Dversion=1.3.3 \
-Dpackaging=maven-archetype
c.创建项目
mvn archetype:generate \
-DgroupId=com.khict \
-DartifactId=kh4j-cloud-demo \
-Dversion=1.3.3 \
-Dpackage=com.khict.demo \
-DarchetypeGroupId=com.khict \
-DarchetypeArtifactId=kh4j-cloud-archetype-service \
-DarchetypeVersion=1.3.3 \
-DarchetypeCatalog=local \
-DinteractiveMode=false
-------------------------------------------------------------------------------------------------
| 参数 | 说明
|-------------------------|-------------------------------
| DgroupId | 项目组织标识
| DartifactId | 项目名称(服务名规范:kh4j-cloud-{业务模块})
| Dversion | 初始版本号
| Dpackage | 基础包路径(建议:com.khict.{业务模块})
| DarchetypeGroupId | 脚手架组织标识
| DarchetypeArtifactId | 脚手架项目标识
| DarchetypeVersion | 脚手架版本(必须与pom中版本一致)
| DarchetypeCatalog=local | 强制使用本地仓库的脚手架模板
| DinteractiveMode=false | 禁用交互模式(批量执行时必需)
d.创建项目,自定义项目名
mvn archetype:generate \
-DgroupId=com.khict \
-DartifactId=kh4j-cloud-onboard \
-Dversion=1.3.3 \
-Dpackage=com.khict.onboard \
-DarchetypeGroupId=com.khict \
-DarchetypeArtifactId=kh4j-cloud-archetype-service \
-DarchetypeVersion=1.3.3 \
-DarchetypeCatalog=local \
-DinteractiveMode=false
b.kh4j-product-onboard-ui
a.注册
a.npm
npm:npm set registry http://172.17.8.54/
npm:npm adduser --registry http://172.17.8.54/
npm:npm profile set password --registry http://172.17.8.54/
b.yarn
Yarn 经典版本的配置不同于 Yarn 2+。如需了解更多,请访问 Yarn 经典版本。
yarn config set registry http://172.17.8.54/
Yarn Berry 不支持 --registry 这个参数,而是所有的配置需要定义在项目根目录下的 yarnrc.yaml 文件里。如需了解更多,请访问 Yarn Berry。
// .yarnrc.yml
npmRegistryServer: "http://172.17.8.54/"
unsafeHttpWhitelist:
- 172.17.8.54
c.pnpm
pnpm:pnpm set registry http://172.17.8.54/
pnpm:pnpm adduser --registry http://172.17.8.54/
pnpm:pnpm profile set password --registry http://172.17.8.54/
b.模块
a.@khlc/common-core
npm install@khlc/[email protected]
yarn add @khlc/[email protected]
pnpm install @khlc/[email protected]
b.@khlc/common-resource
npm install @khlc/[email protected]
yarn add @khlc/[email protected]
pnpm install @khlc/[email protected]
c.@khlc/common-visualcode
npm install @khlc/[email protected]
yarn add @khlc/[email protected]
pnpm install @khlc/[email protected]
d.@khlc/components
npm install @khlc/[email protected]
yarn add @khlc/[email protected]
pnpm install @khlc/[email protected]
e.@khlc/kh-ui
npm install @khlc/[email protected]
yarn add @khlc/[email protected]
pnpm install @khlc/[email protected]
f.@khlc/markdown-editor
npm install @khlc/[email protected]
yarn add @khlc/[email protected]
pnpm install @khlc/[email protected]
g.@khlc/mobile-platform
npm install @khlc/[email protected]
yarn add @khlc/[email protected]
pnpm install @khlc/[email protected]
h.@khlc/platform
npm install @khlc/[email protected]
yarn add @khlc/[email protected]
pnpm install @khlc/[email protected]
i.@khlc/types
npm install @khlc/[email protected]
yarn add @khlc/[email protected]
pnpm install @khlc/[email protected]
j.@khlc/utils
npm install @khlc/[email protected]
yarn add @khlc/[email protected]
pnpm install @khlc/[email protected]
-------------------------------
const utils = require('utils');
k.element-plus
npm install [email protected]
yarn add [email protected]
pnpm install [email protected]
c.使用
a.npm
npm install @khlc/[email protected]
npm install @khlc/[email protected]
npm install @khlc/[email protected]
npm install @khlc/[email protected]
npm install @khlc/[email protected]
npm install @khlc/[email protected]
npm install @khlc/[email protected]
npm install @khlc/[email protected]
npm install @khlc/[email protected]
npm install @khlc/[email protected]
npm install [email protected]
b.yarn
yarn install @khlc/[email protected]
yarn install @khlc/[email protected]
yarn install @khlc/[email protected]
yarn install @khlc/[email protected]
yarn install @khlc/[email protected]
yarn install @khlc/[email protected]
yarn install @khlc/[email protected]
yarn install @khlc/[email protected]
yarn install @khlc/[email protected]
yarn install @khlc/[email protected]
yarn install [email protected]
c.pnpm
pnpm install @khlc/[email protected]
pnpm install @khlc/[email protected]
pnpm install @khlc/[email protected]
pnpm install @khlc/[email protected]
pnpm install @khlc/[email protected]
pnpm install @khlc/[email protected]
pnpm install @khlc/[email protected]
pnpm install @khlc/[email protected]
pnpm install @khlc/[email protected]
pnpm install @khlc/[email protected]
pnpm install [email protected]
02.项目配置
a.管理平台
a.数据链路
子服务(9931) -> 网关(9000) -> 晋钢信息化综合管理平台 + 通过nacos来桥接,子服务去注册,网关去发现 -> 网关(9000) -> 前端(8001)
b.第1步:注册应用
位置:系统管理 -> 菜单程序 -> 子系统管理 -> 新建
-------------------------------------------------------------------------------------------------
上级菜单:根模块
路由:onboard
系统名称:入职管理
菜单名称编码:ruzhiguanli
显示排序:15
是否隐藏:否
c.第2步:注册子服务(kh4j-product-onboard)
位置:系统管理 -> 公共配置 -> 系统字典 -> 左侧【系统词典】的【服务信息」 -> 服务列表 -> 新增服务
选中第1步中注册的应用:入职管理
-------------------------------------------------------------------------------------------------
词典类型:服务列表
strKey:kh4j-product-onboard
服务中文名:入职管理
服务中文名编码:sysDict.dtl.strValue.serviceInfo_kh4j-product-onboard_ruzhiguanli
包扫描路径:com.khict.product.onboard
前缓:/onboard
-------------------------------------------------------------------------------------------------
词典类型:服务列表
strKey:kh4j-product-onboard-J057117
服务中文名:入职管理-J057117
服务中文名编码:sysDict.dtl.strValue.serviceInfo_kh4j-product-onboard-J057117_ruzhiguanlij057117
包扫描路径:com.khict.product.onboard
前缓:/onboard
-------------------------------------------------------------------------------------------------
词典类型:服务列表
strKey:kh4j-product-onboard-J057240
服务中文名:入职管理-J057240
服务中文名编码:sysDict.dtl.strValue.serviceInfo_kh4j-product-onboard-J057240_ruzhiguanlij057240
包扫描路径:com.khict.product.onboard
前缓:/onboard
d.第3步:收集后端API接口
位置:系统管理 -> 权限设置 -> 接口管理
-------------------------------------------------------------------------------------------------
筛选:服务名称 -> 入职管理 / 入职管理-J057117 / 入职管理-J057240
操作:编码接口 -> 服务名称:入职管理 -> 指定路径:/** -> 收集
-------------------------------------------------------------------------------------------------
查看照片文件 GET /onboard/applicant/image/view/photo/** 入职管理 默认动作
白名单:是
免登白名单:是
e.第4步:配置前端Page页面
查看:显示模块
新建:目录 / 菜单
位置:系统管理 -> 菜单程序 -> 菜单管理 -> 应用【入职管理】 -> 应聘管理:面试分配 + 面试评估 + 面试录用
-------------------------------------------------------------------------------------------------
上级菜单:应聘管理
菜单图标:空白
应用选择:入职管理
使用缓存:使用
页面挑选框:请选择
显示排序:1
菜单路径:onbAllocate
菜单名称:面试分配
菜单名称编码:menu:ruzhiguanli:mianshifenpei
组件路径:onboard/onbAllocate/index
权限标识:onbAllocate
是否隐藏:否
b.子服务:kh4j-cloud-gateway
a.使用说明
a.不同环境
本地IDEA -> 网关使用 -> kh4j-cloud-gateway-J057117-uat.yml
生产环境 -> 网关使用 -> kh4j-cloud-gateway-uat.yml
b.接口调用
前端页面 -> 调用子服务(其他人的电脑) -> 走网关
前端页面 -> 调用自己的服务(自己的电脑)-> 自己的服务器+端口
c.访问
后端直接IP+端口:http://localhost:9931/onboard/list?current=1&size=5
前端走vite.config.ts配置:http://172.17.8.57:9000/onboard/list?current=1&size=5
b.本地服务:kh4j-cloud-gateway-service 的 bootstrap.yml
spring:
application:
name: kh4j-cloud-gateway-J057117
cloud:
nacos:
config:
server-addr: 172.17.8.57:8848
file-extension: yml
group: kh4j
username: nacos
password: JinG@ng2025
shared-configs:
- dataId: application-uat.yml
group: kh4j
- dataId: kh4j-cloud-gateway-J057117-uat.yml # 使用kh4j-cloud-gateway-J057117-uat.yml
group: kh4j
namespace: uat
discovery:
server-addr: 172.17.8.57:8848
group: kh4j
namespace: uat
username: nacos
password: JinG@ng2025
profiles:
active: uat
main:
allow-bean-definition-overriding: true
web-application-type: reactive
boot:
admin:
client:
enabled: false
c.网关列表:nacos 的 kh4j-cloud-gateway-J057117-uat.yml
server:
port: 9000
spring:
cloud:
gateway:
routes:
# 招聘管理
- id: smartpark-card
uri: lb://kh-smartpark-card
predicates:
- Path=/card/**
filters:
- RewritePath=/card(?<segment>/?.*), $\{segment}
# 入职管理
- id: product_onboard
uri: lb://kh4j-product-onboard-J057117 # 对应【bootstrap.yml】的【spring.application.name】
predicates:
- Path=/onboard/**
filters:
- RewritePath=/onboard(?<segment>/?.*), $\{segment}
d.服务列表:nacos 的 kh4j-cloud-system-uat.yml
server:
port: 9001
...... 使用公共,无需再配置
kh4j:
system:
service:
relations:
- parentService: kh4j-cloud-hrms
childrenService:
- kh4j-cloud-hrms-025966
- kh4j-cloud-hrms-028114
- kh4j-cloud-hrms-409102
- kh4j-cloud-hrms-028106
- parentService: kh4j-product-onboard # 服务列表
childrenService:
- kh4j-product-onboard-J057117 # 负载均衡1,对应【bootstrap.yml】的【spring.application.name】
- kh4j-product-onboard-J057240 # 负载均衡2,对应【bootstrap.yml】的【spring.application.name】
- parentService: kh4j-product-performance
childrenService:
- kh4j-product-performance-027198
c.子服务:kh4j-product-onboard
a.本地服务:kh4j-product-onboard-service 的 bootstrap.yml
spring:
application:
name: kh4j-product-onboard-J057117
cloud:
nacos:
config:
server-addr: 172.17.8.57:8848
file-extension: yml
group: kh4j
shared-configs:
# # 主要yml配置
- dataId: application-uat.yml
group: kh4j
# # 主要yml中的sql配置
- dataId: datasource-onboard-uat.yml
group: kh4j
- dataId: seata-uat.yml
group: kh4j
namespace: uat
# 项目yml扩展配置,你设置了前缀为 kh4j-cloud-onboard,应用名是 kh4j-cloud-onboard-J057117,那么应用会自动去加载以 kh4j-cloud-onboard 开头的配置文件,比如 kh4j-cloud-onboard-J057117-uat.yml
prefix: kh4j-product-onboard
discovery:
server-addr: 172.17.8.57:8848
group: kh4j
namespace: uat
username: nacos
password: JinG@ng2025
profiles:
active: uat
main:
allow-bean-definition-overriding: true
b.主要yml配置:nacos 的 application-uat.yml
kh4j:
...... 使用公共,无需再配置
loadbalancer: # K8S中的负载均衡
classPath: com.khict.starter.config.IpProximityLoadBalancerConfig
serviceName: kh4j-cloud-system,kh4j-cloud-gateway,kh4j-cloud-hrms,kh4j-cloud-auth,kh4j-cloud-code,kh4j-cloud-store,kh4j-cloud-oams,kh4j-cloud-flow,kh4j-cloud-khapi,kh4j-cloud-datav,kh4j-cloud-devops-monitor,khip-mainpage
c.主要yml中的sql配置:nacos 的 datasource-onboard-uat.yml
spring:
# ===============================================================
# Jackson JSON 序列化/反序列化配置 (Jackson JSON Serialization/Deserialization Configuration)
# ===============================================================
jackson:
# 全局设置日期类型(如 Date, LocalDateTime)序列化为JSON字符串时的格式
date-format: yyyy-MM-dd HH:mm:ss
# 设置时区,保证日期时间处理的一致性,GMT+8是中国标准时间
time-zone: GMT+8
serialization:
# 设置为false,表示将日期序列化为字符串格式(如 "2025-08-20 20:20:20"),而不是时间戳(如 1755778820000)
write-dates-as-timestamps: false
# 设置为false,当遇到空的Java对象时不抛出异常,允许序列化空对象(如只有getter没有属性的对象)
fail-on-empty-beans: false
deserialization:
# 设置为false,表示当JSON字符串中包含Java对象没有的属性时,不抛出异常,而是直接忽略
fail-on-unknown-properties: false
# 支持BigDecimal(已有,正确)- 设置为true时,JSON中的浮点数将被反序列化为BigDecimal而不是Double,提供更高的精度
use-big-decimal-for-floats: true
# 设置为true,允许将单个值作为数组处理。例如:JSON中 "items": "single" 可以反序列化到 List<String> items
accept-single-value-as-array: true
# 设置为true,将空字符串("")视为null对象,防止空字符串导致的反序列化异常
accept-empty-string-as-null-object: true
# 解析器配置,提高容错性
parser:
# 设置为true,允许JSON字符串中包含注释(// 或 /* */),提高配置文件的可读性
allow-comments: true
# 设置为true,允许JSON字符串使用单引号包围字段名和字符串值,而不仅仅是双引号
allow-single-quotes: true
# 设置为true,允许JSON字段名不使用引号包围(如 {name: "value"} 而不是 {"name": "value"})
allow-unquoted-field-names: true
# 全局属性包含策略 - NON_NULL表示序列化时忽略值为null的字段,减少JSON体积
default-property-inclusion: NON_NULL
# 映射器级别配置,用于控制Jackson ObjectMapper的行为
mapper:
# 设置为true,启用大小写不敏感的属性映射。JSON中的 "UserName" 可以映射到 Java 的 "username" 字段
accept-case-insensitive-properties: true
datasource:
dynamic:
primary: master
strict: true
datasource:
master:
# url: jdbc:oracle:thin:@172.17.8.56:1521/jgdb
# username: JGTUSER
# password: JGTUSER#2025
# driver-class-name: oracle.jdbc.driver.OracleDriver
url: jdbc:mysql://172.17.12.67/kh4j-product-onboard?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: K9@pR3!sT7$qL2%
driver-class-name: com.mysql.cj.jdbc.Driver
onboard: # 新增的 MySQL 数据源
url: jdbc:mysql://172.17.12.67/kh4j-product-onboard?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: K9@pR3!sT7$qL2%
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://172.17.12.67/kh4j-product-onboard?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: K9@pR3!sT7$qL2%
druid:
keepAlive: true
initial-size: 5
min-idle: 5
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 开启日志记录到info中,默认实现类DruidDataSourceStatLoggerImpl,info记录日志
#timeBetweenLogStatsMillis: 60000
# maxEvictableIdleTimeMillis: 360000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 'x' FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
logAbandoned: true
removeAbandoned: true
removeAbandonedTimeout: 1800
# 打开PSCache,并且指定每个连接上PSCache的大小
# 官方文档表示oracle下有提升,mysql下建议关闭
poolPreparedStatements: false
maxPoolPreparedStatementPerConnectionSize: -1
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,log4j2
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# aop监控
# aop-patterns: com.nisco.dms.base.service.*,com.nisco.dms.business.service.*
# 配置DruidStatFilter
web-stat-filter:
enabled: true
url-pattern: "/*"
# 不统计的url
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
# 配置DruidStatViewServlet
# ===============================================================
# MyBatis-Plus ORM 框架配置
# ===============================================================
mybatis-plus:
# 指定 Mapper XML 文件的位置。classpath: 表示从类路径下查找
mapper-locations: classpath:/mybatis/*Mapper.xml
# 实体类(Entity)所在的包路径。配置后可在 Mapper XML 中直接使用类名作为别名
type-aliases-package: com.khict.product.onboard.api.entity
# 枚举类所在的包路径。MyBatis-Plus 会自动扫描并处理这些枚举类型与数据库字段的映射
type-enums-package: com.khict.product.onboard.api.enums
# 自定义 TypeHandler 所在包。MyBatis-Plus 启动时会扫描此包下所有继承 BaseTypeHandler,并标注 @MappedJdbcTypes 的类,自动注册到全局配置,用于复杂类型(如 JSON 与对象)映射
type-handlers-package: com.khict.product.onboard.api.hander
# 全局策略配置
global-config:
# ---------- 数据库相关配置 ----------
db-config:
# 主键生成策略:
# AUTO(0): 数据库自增
# INPUT(1): 用户输入
# ASSIGN_ID(2): 雪花算法
# ASSIGN_UUID(3): UUID 字符串
id-type: ASSIGN_UUID
# 字段插入和更新策略:
# IGNORED(0): 忽略判断
# NOT_NULL(1): 只对非 NULL 字段操作
# NOT_EMPTY(2): 只对非空(非 NULL 且非空串/空集合)字段操作
field-strategy: 2
# ❌ 已废弃:3.4.x版本后不再生效,功能已迁移至 configuration.map-underscore-to-camel-case
# 原因:为了与 MyBatis 原生配置保持一致,避免配置重复和混乱
# db-column-underline: true
# ❌ 已废弃:3.4.x版本后功能已内置,无需手动配置
# 原因:MyBatis-Plus 会自动监听 XML 文件变化,该配置项已无实际作用
# refresh-mapper: true
# ❌ 已废弃:3.4.x版本后配置方式发生变化
# 原因:该配置项在新版本中被重新设计,改为更细粒度的控制
# capital-mode: true
# 逻辑删除功能配置:
# 实体类属性名,对应数据库逻辑删除字段
logic-delete-field: deletedFlag
# 逻辑删除时字段值
logic-delete-value: 1
# 未删除时字段值
logic-not-delete-value: 0
# ---------- 自动填充策略(MetaObjectHandler) ----------
# 注册实现了 MetaObjectHandler 接口的 Bean 名称或类,可在插入/更新时自动填充字段
# meta-object-handler: com.khict.product.onboard.config.MyMetaObjectHandler
# MyBatis 原生配置
configuration:
# 开启驼峰命名自动映射(替代已废弃的 db-column-underline)
map-underscore-to-camel-case: true
# 是否开启 MyBatis 二级缓存(默认关闭)
cache-enabled: false
# 查询结果为 NULL 时,是否调用实体 setter(可触发默认值)
call-setters-on-nulls: true
# 指定 MyBatis 使用的日志实现,将 SQL 输出到控制台
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 一级缓存作用域(SESSION 或 STATEMENT)
# local-cache-scope: STATEMENT
# 默认枚举类型处理器(修正为3.4.3.4版本正确路径)
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
pagehelper:
# helperDialect: oracle
reasonable: true
params: count=countSql
# 默认false,当为true时,自动检验适合的数据库
auto-dialect: mysql
# 这个一定要加上,不然mysql和oracle分页两个只能用一个,另一个会报错,加上后,两中数据库分页都可以用了
auto-runtime-dialect: true
support-methods-arguments: true
# ===============================================================
# 自定义日期格式映射配置 (DateFormatConfig.class)
# ===============================================================
date-format:
patterns:
# 仅日期格式的字段(支持通配符)
date-only:
- birthDate
- dischargeDate
- availableDate
- fillDate
- "*Date" # 通配符:以Date结尾
- "*Birth*" # 通配符:包含Birth
- "expire*" # 通配符:以expire开头
# 日期时间格式的字段
date-time:
- createTime
- updateTime
- "*Time" # 通配符:以Time结尾
- "*DateTime" # 通配符:以DateTime结尾
- "start*" # 通配符:以start开头
- "end*" # 通配符:以end开头
formats:
date-only: "yyyy-MM-dd"
date-time: "yyyy-MM-dd HH:mm:ss"
d.项目yml扩展配置:nacos 的 kh4j-product-onboard-uat.yml
server:
port: 9931
servlet:
encoding:
# 设置HTTP请求和响应的默认字符编码
charset: UTF-8
# 启用编码支持
enabled: true
# 强制对所有请求和响应使用配置的字符编码
force: true
...... 使用公共,无需再配置
# ===============================================================
# 系统配置 (System Configuration)
# upload 配置
# ❯ sudo mkdir -p /opt/upload/onboard/{default,attachments}
# ❯ sudo chown -R $USER:staff /opt/upload/onboard,如 sudo chown -R troyesivens:staff /opt/upload/onboard
# ❯ sudo chmod -R 775 /opt/upload/onboard
# ===============================================================
onboard:
upload:
base-path: /opt/upload/onboard # 根路径:所有biz的一层目录根
default-biz: default # 默认biz名称(未传biz时使用)
max-file-size: 20 # 最大文件大小 (MB)
allowed-file-types: jpg,jpeg,png,gif,webp,svg,pdf,doc,docx,xls,xlsx,ppt,pptx # 允许文件类型
ejia:
# eid
eid: 35315462
# 基础地址
base-url: https://e.myslayers.cn/
# 部门获取地址
depart-get-url: https://e.myslayers.cn/gateway/openimport/open/dept/getall?accessToken=%s
# 用户获取地址
user-get-by-depart-url: https://e.myslayers.cn/gateway/openimport/open/person/getall?accessToken=%s
# 获取 accessToken 地址
access-token-url: https://e.myslayers.cn/gateway/oauth2/token/getAccessToken
# 刷新 accessToken 地址
refresh-token-url: https://e.myslayers.cn/gateway/oauth2/token/refreshToken
d.前端:kh4j-product-onboard-ui
a.dist的index.html
npm到nginx,通过【npm run build】打包的【dist的index.html】中设置
reportURL: 'http://localhost:9974/pisces-jimu',
domianURL: 'http://localhost:9994/pisces-boot',
根据不同的url,同样需要nginx来设置类似【vite.config.ts】中的【proxy】不同前缀代理
b.vite.config.ts
server: {
hmr: true,
port: 8001,
// port: 10002,
host: "0.0.0.0",
// 规则一:处理 /dev-api/onboard/ 的请求
// 目标: http://localhost:9931
// 特点: 保留 /dev-api 路径前缀
'/dev-api/onboard/': {
target: 'http://localhost:9931',
changeOrigin: true,
// !!! 注意:这里没有 rewrite 配置 !!!
// 这将导致请求路径被完整地转发
},
// 规则二:处理所有其他的 /dev-api/ 请求(回退到网关)
// 目标: http://127.0.0.1:9000
// 特点: 去除 /dev-api 路径前缀
proxy: {
"/dev-api": {
target: "http://10.70.33.39:9000", // 晋钢测试ip,来自【npm run dev】显示的【内网IP】
// target: "http://127.0.0.1:9000", // 本地ip,进不去【DEBUG】
// target: "http://172.17.8.57:9000", // 测试服务器,发布
rewrite: (p) => p.replace(/^\/dev-api/, ""),
},
},
},
-------------------------------------------------------------------------------------------------
环境 ENV变量值 baseApi前缀 说明
dev "dev" "/dev-api" 开发调试/本地测试专用
prod "prod" "/prod-api" 线上正式环境接口,用户真实访问
uat "uat" "/uat-api" 预发布或验收测试环境,专用于上线前验证
-------------------------------------------------------------------------------------------------
环境变量通过 loadEnv(mode, process.cwd(), "ENV") 加载,接着 baseApi 被赋值为 "/" + config["ENV"] + "-api"
如果 config["ENV"](即 ENV 环境变量)为 "dev" 且执行 build 流程,则 baseApi = "/uat-api"(即特殊处理上线预发条件)
其它情况下,dev 环境的前缀就是 "/dev-api",prod 环境就是 "/prod-api"(源码拼接结果)
c.src/util/pages.ts
export const getPages = function (): { component: string; label: string }[] {
const ret = [
// ------------------------------------------入职管理--------------------------------------
{
name: "onboard/onbApplicant/index",
label: "应聘登记",
},
{
name: "onboard/onbAllocate/index",
label: "面试分配",
},
{
name: "onboard/onbEvaluate/index",
label: "面试评估",
},
{
name: "onboard/onbOffer/index",
label: "面试录用",
},
{
name: "onboard/onbEvaluateApply/index",
label: "评估审批",
},
{
name: "onboard/onbOfferApply/index",
label: "录用审批",
},
{
name: "onboard/onbOrgPersonInfo/index",
label: "组织人员",
},
{
name: "onboard/onbPostionJob/index",
label: "组织岗位",
},
]
return ret as any
}
c.示例(应聘登记):onboard/onbApplicant/index
# 文件:src/api/action.ts
import request from '@khlc/common-core/src/util/request';
/**
* CRUD action
*/
export const getAction = (url, param) => {
return request({
url: url,
method: 'get',
params: param
});
};
-------------------------------------------------------------------------------------------------
# 文件:src/views/onboard/onbApplicant/js/index.js
export default {
name: 'onbApplicant',
data() {
const baseUrl = '/dev-api';
return {
// 请求路径:枚举
sysAttrListUrl: baseUrl + '/onboard/attr/list', // GET请求
// 请求路径:主表
applicantListUrl: baseUrl + '/onboard/applicant/person/list', // POST请求
applicantDetailUrl: baseUrl + '/onboard/applicant/person/detail', // POST请求
applicantInsertUrl: baseUrl + '/onboard/applicant/person/insert', // POST请求
applicantUpdateUrl: baseUrl + '/onboard/applicant/person/update', // POST请求
applicantDeleteUrl: baseUrl + '/onboard/applicant/person/delete', // POST请求
applicantDeleteBatchUrl: baseUrl + '/onboard/applicant/person/deleteBatch', // POST请求 批量删除/恢复
applicantExportUrl: baseUrl + '/onboard/applicant/person/export', // POST请求
};
},
};
6 入职管理平台
6.1 需求沟通
01.应聘登记A表,应聘者录入
a.开始
应聘单位:
应聘组别:
应聘岗位:
-----------------------------------------------------------------------------------------------------
附件1:身份证
附件2:学位证
附件3:毕业证
附件4:学历证书电子注册备案表
附件5:毕业证在线验证报告
b.基本信息
姓名:
照片:
性别:
出生年月:
民族:
政治面貌:党员 / 团员 / 群众
婚姻状况:已婚 / 未婚
-----------------------------------------------------------------------------------------------------
身高:XX厘米
体重:XXX公斤
视力:左:1.0、右:1.0
血型:
职称:
外语等级:
-----------------------------------------------------------------------------------------------------
联系电话:
身份证号:
可报到时间:
是否退役军人:是 / 否
退役时间:
退役证编号:
-----------------------------------------------------------------------------------------------------
籍贯:XX省 XX市 XX县(区)
现家庭住址:XX省 XX市 XX县(区) XX乡(街道) XX村(路) XX号
户口所在地:XX省 XX市 XX县(区) XX乡(街道) XXXXXXXXXXXX派出所
c.工作经历
起止时间 曾工作单位(全称) 职务 负责业务范围 证明人及联系方式
XXXX年XX月 至 XXXX年XX月
XXXX年XX月 至 XXXX年XX月
XXXX年XX月 至 XXXX年XX月
XXXX年XX月 至 XXXX年XX月
d.培训经历
培训时间 受何培训(或培训名称) 获得何资格/技能等级
XXXX年XX月 至 XXXX年XX月
XXXX年XX月 至 XXXX年XX月
XXXX年XX月 至 XXXX年XX月
XXXX年XX月 至 XXXX年XX月
e.家庭情况
姓名 与本人关系 年龄 工作单位 联系电话
XXX 父亲/母亲
XXX 哥哥/弟弟
XXX 姐姐/妹妹
f.学习经历
起止时间 学历 学校名称 专业 学校学历性质
XXXX年XX月 至 XXXX年XX月
XXXX年XX月 至 XXXX年XX月
XXXX年XX月 至 XXXX年XX月
g.疾病史别
过往史:传染病、过敏史、手术史、其它
现在史:残疾、心理病、色盲、恐高、晕高、癫痫、其它
家族史:心脏病、高血压、糖尿病、其它
职业史:耳鼻喉、肺肝腰、其它
其他史:
无病史需手写:本人无任何病史____________
h.备注
应聘途径:网站______、微信/朋友推荐/社会招聘会/校园招聘会/其他
1.是否同意担任轮班工作:是/否
2.是否需住宿:是/否,说明:因宿舍房间有限,恕无法保证均能获得分配。
i.申明
1.本人声明上述资料所填皆属真实、准确,且本人无吸毒、偷盗、赌博、扰乱公共秩序等行为,
如有查证必要时,本人授权同意山西晋城钢铁控股集团有限公司依据个人信息保护法之规定,
向相关单位、部门或公司查询本人之相关资料,如有不实,本人愿意接受(不雇佣)之处理。
2.若经贵公司录用,悉接受分发工作单位绝无异议,入职后同意缴纳社会保险,
且于报到后若有发现体检表上所未记载而属不适应工作之疾病,愿即接受(不雇用)之处理。
3.本人签字:
4.填表时间:XXXX年XX月XX日
02.应聘登记B表,文员反馈【面试结果】
a.基本信息
a.分类1
应聘人:
应聘岗位:
应聘职级:
b.分类2
定编:
缺员:
b.用人单位结构面试(Y6)
a.测试方式
笔试 / 口试 / 实际操作
b.基础素质
a.学历水平
高中 1分
中专 3分
专科 5分
本科 7分
硕士 10分
b.专业匹配
1分 / 3分 / 5分 / 7分 / 10分
c.专业深度
a.理论知识
1分 / 3分 / 5分 / 7分 / 10分
b.实务经验
1分 / 3分 / 5分 / 7分 / 10分
d.经验匹配
a.行业经验
0~2年 1分
3年 3分
4年 5分
5年 7分
6年及以上 10分
e.综合素质
a.目标管理
1分 / 3分 / 5分 / 7分 / 10分
b.计划管理
1分 / 3分 / 5分 / 7分 / 10分
c.组织管理
1分 / 3分 / 5分 / 7分 / 10分
d.绩效管理
1分 / 3分 / 5分 / 7分 / 10分
e.性格管理
1分 / 3分 / 5分 / 7分 / 10分
f.其他信息
a.是否录用
是 / 否
b.试用期
一个月 / 二个月 / 三个月 / 六个月
c.综合分
XX分
c.用人单位结构面试
a.测试方式
笔试 / 口试 / 实际操作
b.专业技能
a.专业知识
1分 / 3分 / 5分 / 7分 / 10分
b.实操技能
1分 / 3分 / 5分 / 7分 / 10分
c.基本素质
a.精神面貌
1分 / 3分 / 5分 / 7分 / 10分
b.吃苦耐劳
1分 / 3分 / 5分 / 7分 / 10分
c.沟通表达
1分 / 3分 / 5分 / 7分 / 10分
d.经验匹配
a.行业经验
0~1年 1分
2年 2分
3年 3分
4年 4分
5年 5分
b.经验分
XX分
e.其他信息
a.用工性质
一般普工 / 一般技工 / 熟练技工
b.是否录用
是 / 否
c.试用期
一个月 / 二个月 / 三个月 / 六个月
d.综合分
XX
6.2 后端设计
01.目录结构
a.人员信息
组织架构管理:与其他模块数据一致,需要每天同步
人员基础信息管理:与其他模块数据一致,需要每天同步
b.招聘管理
应聘登记
面试评估
录用反馈
02.通用界面
a.字段
学历
状态
时间(倒序)
审核状态:待分配(默认)-> 面试中 -> 面试失败/面试成功 -> 待入职 / 面试违规
-----------------------------------------------------------------------------------------------------
操作:审批 / 编辑 / 删除
弹框:发起 / 保存 / 取消
b.权限
用户管理员:人力6人
普通管理员(用户):绑定Y6-李敏,J047813
c.功能1
增删改查
导出
03.审核流程
a.第1步:应聘者录入 --扫码
晋钢轻云办
-> 招聘
-> 应聘登记表
-> 表单,跟【应聘登记】界面中【新增功能】一致
b.第2步:人力资源中心(6人),进行【分配】 --用户管理员
应聘登记
-> 多选:2、3、5
-> 点击【分配】按钮
-> 弹框详情:
1.部门 W1/Y6,一级单位
2.分配人:李敏
-> 【保存/通知/取消】
1.调用【通知,并非工作流】:晋钢ejia -> 抛给Y6李敏,通知消息(人力资源已分配XX、XX参加面试)
2.回调成功:审核状态(待分配 -> 面试中),隐藏2个字段,字段1(一级分配部门),字段2(暂定)
c.第3步:面试中
1.【系统指定】分配给【李敏】,从【研发中心】带人来【管控中心】,进行Y6面试
2.【各个厂处】内部指定【面试官】,对【应聘者】进行【机试+笔试】
3.【面试官】反馈给【李敏】
d.第4步:李敏,进行【录入】 --普通管理员(用户)
1.登录 -> 没有【新增改】,只能看到【分配的3条数据】
2.点击【反馈】按钮,弹框详情如下:
2.1.面试结果:状态(录用/不录用)
2.2.面试建议:XXX
2.3.应聘岗位:XXX(下拉框,一级部门 -> 每个部门的岗位信息)
3.录用:按钮【保存/发起/取消】
3.1.保存
3.1.1.暂存数据
3.2.发起
3.2.1.审核状态:待分配 -> 面试中
3.2.2.发起ejia审批
名称(XX应聘结果审批)
发起人:李敏
审批人1:李敏(暂定)
审批人2:由前一级审批人(李敏),来指定对应【二级主管】
审批人3:李敏的一级主管(该一级主管可直接找到)
审批状态:面试中 -> 面试成功
3.2.取消
3.1.1.取消弹框
4.不录用:按钮【保存/取消】
4.1.保存
4.1.1.由人力再去决定【应聘哪个岗位】,分配人【从李敏变成NULL】
4.2.取消
4.1.1.取消弹框
e.第5步:人力资源中心(6人),进行【补录】 --用户管理员
1.登录 -> 有【新增改】,可以看到【面试成功】的【3条数据】
2.操作【张三】
3.点击【编辑】按钮,弹框详情如下:
3.1.状态:录用
3.2.试用期工资:XXX
3.3.试用期时长:1个月/2个月/3个月
3.4.转正工资:XXXX
3.5.用工性质:一般普工/一般技工/熟练技工
4.点击【审批】按钮,发起ejia审批
名称(XX录用结果审批)
发起人:HR(用户管理员)
审批人1:王海英(人资处长)
审批人2:王海英的一级主管(该一级主管可直接找到)
审批人3:詹总
审批状态:面试成功 -> 待入职
f.附:定时操作
a.待分配
说明:3天内,应聘者未到指定地点参与应聘
状态:待分配 -> 面试违规
b.待分配 -> 面试中
说明:3天内,面试失败
状态:待分配 -> 面试中 -> 面试违规
c.待入职
说明:需要同步这部分数据到【ERP】
状态:同步操作
6.3 前端设计
00.整体框架
a.风格:上中下
筛选框+多个按钮(查询、重置、新增)
表格:列1、列2、列3、列4,操作1:XX、XX、XX,操作2:XX
分页组件
b.弹框1:普通表单(单列/多列,自动根据字段多少来确定)
字段1:值1
字段2:值2
字段3:值3
c.弹框2:有多个标签页,每个标签页都有表单(单列/多列,自动根据字段多少来确定) / 表格(子表,完整的增删改查)
标签页1
字段1:值1 字段4:值1
字段2:值2 字段5:值2
字段3:值3 字段6:值2
标签页2
字段1:值1 字段4:值1
字段2:值2 字段5:值2
字段3:值3 字段6:值2
标签页3
字段1:值1 字段4:值1
字段2:值2 字段5:值2
字段3:值3 字段6:值2
01.应聘登记
a.区域1:筛选框
姓名
性别(1-男、2-女)
出生日期
联系电话
身份证号
政治面貌(1-党员、2-团员、3-群众)
外语等级(1-一般、2-四级、3-六级、4-专四、5-专八、6-托福、7-雅思)
应聘岗位
审核状态(1-待分配、2-等待面试)
分配人
报到时间
面试时间
b.区域2:表格
姓名
照片
性别
出生日期
联系电话
身份证号
应聘岗位
审核状态(1-待分配、2-等待面试)
分配人
报到时间
面试时间
-----------------------------------------------------------------------------------------------------
操作1:详情、编辑、删除
操作2:分配、审核、撤回审核
c.区域2:操作1:详情/编辑(仅仅表单共用)
标题:详情/编辑
-----------------------------------------------------------------------------------------------------
标签页1:基本情况
以下是表单(多列)
applicant_name 姓名
apply_position 应聘岗位
photo_url 照片URL
gender 性别(1-男、2-女)
birth_date 出生日期
nation 民族
political_status 政治面貌(1-党员、2-团员、3-群众)
marital_status 婚姻状况(1-已婚、2-未婚)
height 身高(厘米)
weight 体重(公斤)
vision_left 左眼视力
vision_right 右眼视力
blood_type 血型(1-A型、2-B型、3-AB型、4-O型)
professional_title 职称
foreign_language_level 外语等级(1-一般、2-四级、3-六级、4-专四、5-专八、6-托福、7-雅思)
foreign_language_score 外语分数
contact_phone 联系电话
id_card_no 身份证号
available_date 可报到时间
is_veteran 是否退役军人(0-否、1-是)
discharge_date 退役时间
discharge_cert_no 退役证编号
home_address 现家庭住址
household_register 户口所在地
native_place 籍贯
-----------------------------------------------------------------------------------------------------
标签页2:工作经历
以下是表格(多列)
start_date 开始时间
end_date 结束时间
company_name 曾工作单位全称
position 职务
business_scope 负责业务范围
reference_person 证明人
reference_contact 证明人的联系方式
操作:编辑、删除
-----------------------------------------------------------------------------------------------------
标签页3:培训经历
以下是表格(完整增删改查)
按钮:新增
start_date 培训开始时间
end_date 培训结束时间
training_name 受何培训/培训名称
qualification 获得何资格/技能等级
操作:编辑、删除
-----------------------------------------------------------------------------------------------------
标签页4:家庭情况
以下是表格(完整增删改查)
按钮:新增
name 姓名
relationship 与本人关系
age 年龄
work_unit 工作单位
contact_phone 联系电话
操作:编辑、删除
-----------------------------------------------------------------------------------------------------
标签页5:学习经历
以下是表格(完整增删改查)
按钮:新增
start_date 开始时间
end_date 结束时间
education_level 学历
school_name 学校名称
major 专业
school_nature 学校/学历类型
操作:编辑、删除
-----------------------------------------------------------------------------------------------------
标签页6:病史类别
past_history 过往史
current_history 现在史
family_history 家族史
occupational_history 职业史
other_history 其他史
no_history_statement 无病史声明
-----------------------------------------------------------------------------------------------------
标签页7:附件信息
以下是5个上传文件的组件,每个组件支持上传、删除、更换
分别有1-身份证、2-学位证、3-毕业证、4-学历证书电子注册备案表、5-毕业证在线验证报告
-----------------------------------------------------------------------------------------------------
标签页8:备注
应聘途径(多选):网站、微信、朋友推荐、社会招聘会、校园招聘会、其他
是否同意轮班工作(单选):是、否
是否需要住宿(单选):是、否
住宿说明:因宿舍房间有限,恕无法保证均能获得分配
-----------------------------------------------------------------------------------------------------
标签页9:申明
声明内容:*本人声明上述资料所填皆属真实、准确,且本人无吸毒、偷盗、赌博、扰乱公共秩序等行为,如有查证必要时,本人授权同意山西晋城钢铁控股集团有限公司依据个人信息保护法之规定,向相关单位、部门或公司查询本人之相关资料,如有不实,本人愿意接受(不雇佣)之处理。
*若经贵公司录用,悉接受分发工作单位绝无异议,入职后同意缴纳社会保险,且于报到后若有发现体检表上所未记载而属不适应工作之疾病,愿即接受(不雇用)之处理。
本人签字:
填表时间:
d.区域2:操作1:删除
标题:删除确认
-----------------------------------------------------------------------------------------------------
说明:确认删除该XXX吗?此操作不可撤销!
按钮1:取消
按钮2:确认删除
e.区域2:操作2:分配
标题:分配面试
-----------------------------------------------------------------------------------------------------
应聘者:XX applicantId
应聘岗位:XX applyPosition
分配人:XX assignedUser
分配部门:XX assignedDept
分配说明:XX assignedReason
-----------------------------------------------------------------------------------------------------
按钮1:取消
按钮2:保存
f.区域2:操作2:审核
弹出确认框,发出POST请求
g.区域2:操作2:撤回审核
弹出确认框,发出POST请求
h.区域3:分页组件
共19条 10条/页 1 2 3 ... 前往X页
02.面试评估
a.区域1:筛选框
姓名
性别(1-男、2-女)
出生日期
联系电话
身份证号
政治面貌(1-党员、2-团员、3-群众)
外语等级(1-一般、2-四级、3-六级、4-专四、5-专八、6-托福、7-雅思)
应聘岗位
审核状态(3-面试审核中、4-面试通过、5-面试失败)
分配人
报到时间
面试时间
b.区域2:表格
姓名
照片
性别
出生日期
联系电话
身份证号
应聘岗位
审核状态(3-面试审核中、4-面试通过、5-面试失败)
分配人
报到时间
面试时间
-----------------------------------------------------------------------------------------------------
操作1:详情、编辑、删除
操作2:评估、审核、撤回审核
c.区域2:操作1:详情/编辑(仅仅表单共用)
标题:详情/编辑
-----------------------------------------------------------------------------------------------------
标签页1:基本情况
以下是表单(多列)
applicant_name 姓名
apply_position 应聘岗位
photo_url 照片URL
gender 性别(1-男、2-女)
birth_date 出生日期
nation 民族
political_status 政治面貌(1-党员、2-团员、3-群众)
marital_status 婚姻状况(1-已婚、2-未婚)
height 身高(厘米)
weight 体重(公斤)
vision_left 左眼视力
vision_right 右眼视力
blood_type 血型(1-A型、2-B型、3-AB型、4-O型)
professional_title 职称
foreign_language_level 外语等级(1-一般、2-四级、3-六级、4-专四、5-专八、6-托福、7-雅思)
foreign_language_score 外语分数
contact_phone 联系电话
id_card_no 身份证号
available_date 可报到时间
is_veteran 是否退役军人(0-否、1-是)
discharge_date 退役时间
discharge_cert_no 退役证编号
home_address 现家庭住址
household_register 户口所在地
native_place 籍贯
-----------------------------------------------------------------------------------------------------
标签页2:工作经历
以下是表格(多列)
start_date 开始时间
end_date 结束时间
company_name 曾工作单位全称
position 职务
business_scope 负责业务范围
reference_person 证明人
reference_contact 证明人的联系方式
操作:编辑、删除
-----------------------------------------------------------------------------------------------------
标签页3:培训经历
以下是表格(完整增删改查)
按钮:新增
start_date 培训开始时间
end_date 培训结束时间
training_name 受何培训/培训名称
qualification 获得何资格/技能等级
操作:编辑、删除
-----------------------------------------------------------------------------------------------------
标签页4:家庭情况
以下是表格(完整增删改查)
按钮:新增
name 姓名
relationship 与本人关系
age 年龄
work_unit 工作单位
contact_phone 联系电话
操作:编辑、删除
-----------------------------------------------------------------------------------------------------
标签页5:学习经历
以下是表格(完整增删改查)
按钮:新增
start_date 开始时间
end_date 结束时间
education_level 学历
school_name 学校名称
major 专业
school_nature 学校/学历类型
操作:编辑、删除
-----------------------------------------------------------------------------------------------------
标签页6:病史类别
past_history 过往史
current_history 现在史
family_history 家族史
occupational_history 职业史
other_history 其他史
no_history_statement 无病史声明
-----------------------------------------------------------------------------------------------------
标签页7:附件信息
以下是5个上传文件的组件,每个组件支持上传、删除、更换
分别有1-身份证、2-学位证、3-毕业证、4-学历证书电子注册备案表、5-毕业证在线验证报告
-----------------------------------------------------------------------------------------------------
标签页8:备注
应聘途径(多选):网站、微信、朋友推荐、社会招聘会、校园招聘会、其他
是否同意轮班工作(单选):是、否
是否需要住宿(单选):是、否
住宿说明:因宿舍房间有限,恕无法保证均能获得分配
-----------------------------------------------------------------------------------------------------
标签页9:申明
声明内容:*本人声明上述资料所填皆属真实、准确,且本人无吸毒、偷盗、赌博、扰乱公共秩序等行为,如有查证必要时,本人授权同意山西晋城钢铁控股集团有限公司依据个人信息保护法之规定,向相关单位、部门或公司查询本人之相关资料,如有不实,本人愿意接受(不雇佣)之处理。
*若经贵公司录用,悉接受分发工作单位绝无异议,入职后同意缴纳社会保险,且于报到后若有发现体检表上所未记载而属不适应工作之疾病,愿即接受(不雇用)之处理。
本人签字:
填表时间:
d.区域2:操作1:删除
标题:删除确认
-----------------------------------------------------------------------------------------------------
说明:确认删除该XXX吗?此操作不可撤销!
按钮1:取消
按钮2:确认删除
e.区域2:操作2:评估
标题:评估
-----------------------------------------------------------------------------------------------------
以下是表单(多列)
applicant_name 姓名
gender 性别(1-男、2-女)
birth_date 出生日期
contact_phone 联系电话
id_card_no 身份证号
applyPosition 应聘岗位
请选择评估类型:A类型/B类型
-----------------------------------------------------------------------------------------------------
根据【请选择评估类型:A类型/B类型】来【显示/隐藏】,下面是 Y6结构面试评估 (A类型)
基础素质评估
学历水平 1分3分5分7分10分 高中1分,中专3分,专科5分,本科7分,硕士10分
专业匹配 1分3分5分7分10分 专业匹配度评估
专业深度评估
理论知识 1分3分5分7分10分 专业理论掌握程度
实务经验 1分3分5分7分10分 实际工作经验
经验匹配评估
行业经验 1分3分5分7分10分 0-2年1分,3年3分,4年5分,5年7分,6年以上10分
综合素质评估
目标管理 1分3分5分7分10分 目标设定和执行能力
计划管理 1分3分5分7分10分 计划制定和管理能力
组织管理 1分3分5分7分10分 组织协调能力
绩效管理 1分3分5分7分10分 绩效管理能力
性格管理 1分3分5分7分10分 性格适应性
总分
XXX
-----------------------------------------------------------------------------------------------------
根据【请选择评估类型:A类型/B类型】来【显示/隐藏】,下面是 用人单位结构面试评估 (B类型)
专业技能评估
专业知识 1分3分5分7分10分 专业知识掌握程度
实操技能 1分3分5分7分10分 实际操作技能
综合素质评估
精神状态 1分3分5分7分10分 精神面貌和状态
吃苦耐劳 1分3分5分7分10分 工作态度和耐力
沟通协调 1分3分5分7分10分 沟通协调能力
工作经验 1分3分5分7分10分 相关工作经验
用工类型
用工类型 ○正式员工 ○劳务派遣 ○临时工 ○实习生
总分
XXX
-----------------------------------------------------------------------------------------------------
测试方式: 1-笔试、2-口试、3-实际操作 test_method
用工性质: 1-一般普工、2-一般技工、3-熟练技工 employment_type
面试结果: 通过/不通过(可以更改)
面试意见: XXX(可以更改)
按钮1:取消
按钮2:保存
f.区域2:操作2:审核
弹出确认框,发出POST请求
g.区域2:操作2:撤回审核
弹出确认框,发出POST请求
h.区域3:分页组件
共19条 10条/页 1 2 3 ... 前往X页
03.录用反馈
a.区域1:筛选框
姓名
性别(1-男、2-女)
出生日期
联系电话
身份证号
政治面貌(1-党员、2-团员、3-群众)
外语等级(1-一般、2-四级、3-六级、4-专四、5-专八、6-托福、7-雅思)
应聘岗位
审核状态(3-面试审核中、4-面试通过、5-面试失败)
分配人
报到时间
面试时间
b.区域2:表格
姓名
照片
性别
出生日期
联系电话
身份证号
应聘岗位
审核状态(3-面试审核中、4-面试通过、5-面试失败)
分配人
报到时间
面试时间
-----------------------------------------------------------------------------------------------------
操作1:详情、编辑、删除
操作2:录用、审核、撤回审核
c.区域2:操作1:详情/编辑(仅仅表单共用)
标题:详情/编辑
-----------------------------------------------------------------------------------------------------
标签页1:基本情况
以下是表单(多列)
applicant_name 姓名
apply_position 应聘岗位
photo_url 照片URL
gender 性别(1-男、2-女)
birth_date 出生日期
nation 民族
political_status 政治面貌(1-党员、2-团员、3-群众)
marital_status 婚姻状况(1-已婚、2-未婚)
height 身高(厘米)
weight 体重(公斤)
vision_left 左眼视力
vision_right 右眼视力
blood_type 血型(1-A型、2-B型、3-AB型、4-O型)
professional_title 职称
foreign_language_level 外语等级(1-一般、2-四级、3-六级、4-专四、5-专八、6-托福、7-雅思)
foreign_language_score 外语分数
contact_phone 联系电话
id_card_no 身份证号
available_date 可报到时间
is_veteran 是否退役军人(0-否、1-是)
discharge_date 退役时间
discharge_cert_no 退役证编号
home_address 现家庭住址
household_register 户口所在地
native_place 籍贯
-----------------------------------------------------------------------------------------------------
标签页2:工作经历
以下是表格(多列)
start_date 开始时间
end_date 结束时间
company_name 曾工作单位全称
position 职务
business_scope 负责业务范围
reference_person 证明人
reference_contact 证明人的联系方式
操作:编辑、删除
-----------------------------------------------------------------------------------------------------
标签页3:培训经历
以下是表格(完整增删改查)
按钮:新增
start_date 培训开始时间
end_date 培训结束时间
training_name 受何培训/培训名称
qualification 获得何资格/技能等级
操作:编辑、删除
-----------------------------------------------------------------------------------------------------
标签页4:家庭情况
以下是表格(完整增删改查)
按钮:新增
name 姓名
relationship 与本人关系
age 年龄
work_unit 工作单位
contact_phone 联系电话
操作:编辑、删除
-----------------------------------------------------------------------------------------------------
标签页5:学习经历
以下是表格(完整增删改查)
按钮:新增
start_date 开始时间
end_date 结束时间
education_level 学历
school_name 学校名称
major 专业
school_nature 学校/学历类型
操作:编辑、删除
-----------------------------------------------------------------------------------------------------
标签页6:病史类别
past_history 过往史
current_history 现在史
family_history 家族史
occupational_history 职业史
other_history 其他史
no_history_statement 无病史声明
-----------------------------------------------------------------------------------------------------
标签页7:附件信息
以下是5个上传文件的组件,每个组件支持上传、删除、更换
分别有1-身份证、2-学位证、3-毕业证、4-学历证书电子注册备案表、5-毕业证在线验证报告
-----------------------------------------------------------------------------------------------------
标签页8:备注
应聘途径(多选):网站、微信、朋友推荐、社会招聘会、校园招聘会、其他
是否同意轮班工作(单选):是、否
是否需要住宿(单选):是、否
住宿说明:因宿舍房间有限,恕无法保证均能获得分配
-----------------------------------------------------------------------------------------------------
标签页9:申明
声明内容:*本人声明上述资料所填皆属真实、准确,且本人无吸毒、偷盗、赌博、扰乱公共秩序等行为,如有查证必要时,本人授权同意山西晋城钢铁控股集团有限公司依据个人信息保护法之规定,向相关单位、部门或公司查询本人之相关资料,如有不实,本人愿意接受(不雇佣)之处理。
*若经贵公司录用,悉接受分发工作单位绝无异议,入职后同意缴纳社会保险,且于报到后若有发现体检表上所未记载而属不适应工作之疾病,愿即接受(不雇用)之处理。
本人签字:
填表时间:
d.区域2:操作1:删除
标题:删除确认
-----------------------------------------------------------------------------------------------------
说明:确认删除该XXX吗?此操作不可撤销!
按钮1:取消
按钮2:确认删除
e.区域2:操作2:录用
标题:录用
-----------------------------------------------------------------------------------------------------
以下是表单(多列)
applicant_name 姓名
gender 性别(1-男、2-女)
birth_date 出生日期
contact_phone 联系电话
id_card_no 身份证号
applyPosition 应聘岗位
-----------------------------------------------------------------------------------------------------
状态 录用/不录用(可以更改)
试用期工资 XXX(可以更改)
试用期时长 1个月/2个月/3个月(可以更改)
转正工资 XXXX
-----------------------------------------------------------------------------------------------------
按钮1:取消
按钮2:保存
f.区域2:操作2:审核
弹出确认框,发出POST请求
g.区域2:操作2:撤回审核
弹出确认框,发出POST请求
h.区域3:分页组件
共19条 10条/页 1 2 3 ... 前往X页