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.优先级机制
    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`

02.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.说明
            优点:性能高、存储空间小、索引友好
            缺点:依赖时钟、时钟回拨问题
            适用:高性能要求、单机或小规模集群

03.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序列化返回给前端

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.jhict.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.jhict.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 日期:DateFormatConfig

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 ref


$

this 作用域

4.2 引入

引入方式
import tab from './tab';
import { VueInstanceAuth } from './auth';
import cache from './cache';
import { VueInstanceDownload } from './download';
import { App } from 'vue';
import { VueInstanceModal } from './modal';

4.3 mixins

src/mixins
https://zhuanlan.zhihu.com/p/482735975


BaseMixins.ts
drawMixin.js
ListMixins.js
ListModalMixins.js
OldBaseMixins.ts
Standard.js
TableMixins.js
TreeListMixins.js
TreeListModalMixins.js

4.4 plugins

src/plugins

// 页签操作
app.config.globalProperties.$tab = tab;
// 认证对象
app.config.globalProperties.$auth = new VueInstanceAuth();
// 缓存对象
app.config.globalProperties.$cache = cache;
// 模态框对象
const modal = new VueInstanceModal();
app.config.globalProperties.$modal = modal;
// 下载文件
app.config.globalProperties.$download = new VueInstanceDownload();

app.config.globalProperties.file_viewer_url = 'http://172.18.248.205:8012/onlinePreview';

app.config.globalProperties.msgSuccess = modal.msgSuccess;
app.config.globalProperties.msgWarning = modal.msgWarning;
app.config.globalProperties.msgError = modal.msgError;

// 新增常用的方法 全局变量
app.config.globalProperties.getAction = getAction;
app.config.globalProperties.postAction = postAction;
app.config.globalProperties.deleteAction = deleteAction;
app.config.globalProperties.putAction = putAction;
app.config.globalProperties.validateNull = validateNull;

4.5 字典配置

this.getDictListData(["meterRegion"]);
getDictListData (strSn) {                src/mixins/ListModalMixins.js:24

ListModalMixins.js          created () { },
ListMixins.js
this.$nextTick(() => {
  if (!this.$refs.modalForm) {
    // this.proxy.$modal.msgWarning('请添加modalForm表单!')
    // return
  }
});
if (this.autoLoadList && this.url && !this.isFangKeCar) {
  this.loadData();
}
if (this.autoLoadDict) {
  //初始化字典配置 在自己页面定义
  this.initDictConfig();
}

4.6 父子组件

src/views/performance/meteringManagement
  mixins: [ListModalMixins, ListMixins],
  components: {
    treeMultiple,
    personSelect,
  },