通用模块工具箱(开箱即用版)

通用模块工具箱(开箱即用版)

  • 通用模块工具箱(开箱即用版)
  • 一、基础核心模块(项目初始化必用,100%通用)
    • [1. 全局配置类](#1. 全局配置类)
      • [1.1 GlobalCorsConfig(跨域配置)](#1.1 GlobalCorsConfig(跨域配置))
      • [1.2 MyBatisPlusConfig(MyBatisPlus配置)](#1.2 MyBatisPlusConfig(MyBatisPlus配置))
      • [1.3 RedisConfig(Redis配置)](#1.3 RedisConfig(Redis配置))
      • [1.4 SecurityConfig(基础安全配置)](#1.4 SecurityConfig(基础安全配置))
      • [1.5 ThreadPoolConfig(通用线程池配置)](#1.5 ThreadPoolConfig(通用线程池配置))
    • [2. 核心枚举类](#2. 核心枚举类)
      • [2.1 ErrorCodeEnum(错误码枚举)](#2.1 ErrorCodeEnum(错误码枚举))
      • [2.2 HttpStatusEnum(HTTP状态码映射)](#2.2 HttpStatusEnum(HTTP状态码映射))
      • [2.3 BusinessStatusEnum(通用业务状态)](#2.3 BusinessStatusEnum(通用业务状态))
    • [3. 基础工具类](#3. 基础工具类)
      • [3.1 StringUtil(字符串工具)](#3.1 StringUtil(字符串工具))
      • [3.2 DateUtil(时间工具)](#3.2 DateUtil(时间工具))
      • [3.3 EncryptUtil(加密工具)](#3.3 EncryptUtil(加密工具))
      • [3.4 NumberUtil(数字工具)](#3.4 NumberUtil(数字工具))
      • [3.5 CollectionUtil(集合工具)](#3.5 CollectionUtil(集合工具))
      • [3.6 BeanUtil(对象转换)](#3.6 BeanUtil(对象转换))
  • 二、接口交互模块(接口开发必用,100%通用)
    • [1. 统一返回相关](#1. 统一返回相关)
      • [1.1 ApiResponse<T>(全局返回体)](#1.1 ApiResponse<T>(全局返回体))
      • [1.2 PageParam(分页请求参数)](#1.2 PageParam(分页请求参数))
      • [1.3 PageResult<T>(分页结果)](#1.3 PageResult<T>(分页结果))
      • [1.4 ApiResultCode(返回码枚举)](#1.4 ApiResultCode(返回码枚举))
    • [2. 参数校验相关](#2. 参数校验相关)
      • [2.1 GroupValidator(分组校验接口)](#2.1 GroupValidator(分组校验接口))
      • [2.2 CustomValidAnnotations(自定义校验注解)](#2.2 CustomValidAnnotations(自定义校验注解))
      • [2.3 ValidUtil(手动校验工具)](#2.3 ValidUtil(手动校验工具))
  • 三、业务逻辑模块(业务开发必用,95%通用)
    • [1. 异常处理](#1. 异常处理)
      • [1.1 GlobalExceptionHandler(全局异常拦截)](#1.1 GlobalExceptionHandler(全局异常拦截))
      • [1.2 BusinessException(业务异常)](#1.2 BusinessException(业务异常))
      • [1.3 SystemException(系统异常)](#1.3 SystemException(系统异常))
      • [1.4 ParamInvalidException(参数异常)](#1.4 ParamInvalidException(参数异常))
    • [2. 业务工具类](#2. 业务工具类)
      • [2.1 ExcelUtil(EasyExcel导入导出)](#2.1 ExcelUtil(EasyExcel导入导出))
      • [2.2 IdGeneratorUtil(分布式ID生成)](#2.2 IdGeneratorUtil(分布式ID生成))
      • [2.3 FileUtil(文件操作工具)](#2.3 FileUtil(文件操作工具))
      • [2.4 MoneyUtil(金额计算工具)](#2.4 MoneyUtil(金额计算工具))
      • [2.5 CacheUtil(本地缓存工具)](#2.5 CacheUtil(本地缓存工具))
  • 四、安全防护模块(上线前必用,95%通用)
    • [1. 安全工具类](#1. 安全工具类)
      • [1.1 XssFilter(防XSS攻击过滤器)](#1.1 XssFilter(防XSS攻击过滤器))
      • [1.2 SensitiveUtil(敏感信息脱敏工具)](#1.2 SensitiveUtil(敏感信息脱敏工具))
      • [1.3 TokenUtil(JWT工具类)](#1.3 TokenUtil(JWT工具类))
      • [1.4 SignUtil(接口签名工具)](#1.4 SignUtil(接口签名工具))
    • [2. 防护组件](#2. 防护组件)
      • [2.1 IdempotentAOP(幂等性注解AOP)](#2.1 IdempotentAOP(幂等性注解AOP))
      • [2.2 AntiBrushInterceptor(接口防刷拦截器)](#2.2 AntiBrushInterceptor(接口防刷拦截器))
      • [2.3 SqlInjectFilter(防SQL注入过滤器)](#2.3 SqlInjectFilter(防SQL注入过滤器))
  • 五、监控审计模块(上线前必用,90%通用)
    • [1. 日志审计](#1. 日志审计)
      • [1.1 OperateLogAOP(操作日志AOP)](#1.1 OperateLogAOP(操作日志AOP))
      • [1.2 TraceIdUtil(全链路TraceId工具)](#1.2 TraceIdUtil(全链路TraceId工具))
      • [1.3 LogFormatter(日志格式化工具)](#1.3 LogFormatter(日志格式化工具))
    • [2. 监控工具](#2. 监控工具)
      • [2.1 MetricsUtil(自定义指标工具)](#2.1 MetricsUtil(自定义指标工具))
      • [2.2 HealthCheckUtil(健康检查工具)](#2.2 HealthCheckUtil(健康检查工具))
      • [2.3 ExceptionLogUtil(异常日志工具)](#2.3 ExceptionLogUtil(异常日志工具))
  • 大厂规范

通用模块工具箱(开箱即用版)

以下是符合大厂标准的工具类完整实现,包含包名规范、核心逻辑、详细注释,并标注「常用度」和「所属层级」(控制层/业务层/DAO层/通用层/配置层):

一、基础核心模块(项目初始化必用,100%通用)

1. 全局配置类

1.1 GlobalCorsConfig(跨域配置)

java 复制代码
package com.xxx.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.Arrays;
import java.util.List;

/**
 * 全局跨域配置(大厂标准:支持多域名、Credentials、自定义请求头)
 * 常用度:100%
 * 所属层级:配置层(通用)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Configuration
public class GlobalCorsConfig implements WebMvcConfigurer {

    /**
     * 允许跨域的域名列表(建议从配置文件读取)
     */
    private static final List<String> ALLOWED_ORIGINS = Arrays.asList(
            "https://www.xxx.com",
            "https://test.xxx.com",
            "http://localhost:8080"
    );

    /**
     * 基于Filter的跨域配置(兼容所有请求类型)
     */
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        // 允许指定域名跨域
        config.setAllowedOrigins(ALLOWED_ORIGINS);
        // 允许携带Cookie
        config.setAllowCredentials(true);
        // 允许所有请求方法
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        // 允许所有请求头
        config.setAllowedHeaders(Arrays.asList("*"));
        // 暴露自定义响应头(如Token)
        config.setExposedHeaders(Arrays.asList("X-Token", "Trace-Id"));
        // 预检请求有效期(秒)
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 所有接口生效
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

    /**
     * 基于WebMvc的跨域配置(兜底)
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins(ALLOWED_ORIGINS.toArray(new String[0]))
                .allowedMethods("*")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

1.2 MyBatisPlusConfig(MyBatisPlus配置)

java 复制代码
package com.xxx.common.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.LocalDateTime;

/**
 * MyBatisPlus核心配置(分页+字段自动填充,兼容3.5.x+,适配MySQL/Oracle)
 * 常用度:100%
 * 所属层级:DAO层(配置)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Configuration
public class MyBatisPlusConfig implements MetaObjectHandler {

    /**
     * 分页插件(适配多数据库)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // MySQL分页
        PaginationInnerInterceptor mysqlPageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        // 溢出总页数后是否进行处理(true:返回最后一页,false:返回空)
        mysqlPageInterceptor.setOverflow(true);
        // 单页最大条数限制(防止恶意分页)
        mysqlPageInterceptor.setMaxLimit(1000L);
        interceptor.addInnerInterceptor(mysqlPageInterceptor);

        // Oracle分页(按需开启)
        // PaginationInnerInterceptor oraclePageInterceptor = new PaginationInnerInterceptor(DbType.ORACLE);
        // interceptor.addInnerInterceptor(oraclePageInterceptor);
        return interceptor;
    }

    /**
     * 新增时自动填充创建时间/更新时间
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }

    /**
     * 更新时自动填充更新时间
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

1.3 RedisConfig(Redis配置)

java 复制代码
package com.xxx.common.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis核心配置(Jackson序列化防乱码、连接池优化)
 * 常用度:100%
 * 所属层级:通用层(配置)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Configuration
public class RedisConfig {

    /**
     * 自定义RedisTemplate(解决序列化乱码问题)
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);

        // Jackson序列化器(支持LocalDateTime等JDK8时间类型)
        Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        // 开启字段可见性
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 开启多类型序列化(避免反序列化类型丢失)
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        // 注册JDK8时间模块
        objectMapper.registerModule(new JavaTimeModule());
        jacksonSerializer.setObjectMapper(objectMapper);

        // String序列化器(Key/HashKey)
        StringRedisSerializer stringSerializer = new StringRedisSerializer();

        // 配置序列化规则
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        redisTemplate.setValueSerializer(jacksonSerializer);
        redisTemplate.setHashValueSerializer(jacksonSerializer);

        // 初始化
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * 配置Lettuce连接池(默认已集成,此处为自定义参数示例)
     */
    @Bean
    public LettuceConnectionFactory lettuceConnectionFactory() {
        // 实际项目中建议从配置文件读取Redis连接信息
        // RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("127.0.0.1", 6379);
        // config.setPassword(RedisPassword.of("xxx"));
        // return new LettuceConnectionFactory(config);
        return (LettuceConnectionFactory) connectionFactory;
    }
}

1.4 SecurityConfig(基础安全配置)

java 复制代码
package com.xxx.common.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

/**
 * SpringSecurity基础配置(忽略Swagger/Actuator、关闭CSRF)
 * 常用度:100%
 * 所属层级:安全层(配置)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    /**
     * 核心安全过滤链
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 关闭CSRF(前后端分离场景)
                .csrf(csrf -> csrf.disable())
                // 忽略无需认证的接口
                .authorizeHttpRequests(auth -> auth
                        // Swagger接口
                        .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
                        // 健康检查接口
                        .requestMatchers("/actuator/**").permitAll()
                        // 公开接口
                        .requestMatchers("/api/v1/public/**").permitAll()
                        // 其余接口需认证(可根据业务调整)
                        .anyRequest().authenticated()
                );
        return http.build();
    }
}

1.5 ThreadPoolConfig(通用线程池配置)

java 复制代码
package com.xxx.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

import java.util.concurrent.*;

/**
 * 通用线程池配置(防OOM、可配置核心/最大线程数)
 * 常用度:100%
 * 所属层级:通用层(配置)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Configuration
@EnableAsync
public class ThreadPoolConfig {

    /**
     * 核心线程数(CPU核心数+1)
     */
    private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() + 1;

    /**
     * 最大线程数(CPU核心数*2)
     */
    private static final int MAX_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2;

    /**
     * 队列容量
     */
    private static final int QUEUE_CAPACITY = 1000;

    /**
     * 空闲线程存活时间(秒)
     */
    private static final int KEEP_ALIVE_TIME = 60;

    /**
     * 通用业务线程池
     */
    @Bean("businessThreadPool")
    public Executor businessThreadPool() {
        return new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(QUEUE_CAPACITY),
                // 自定义线程名称(便于排查问题)
                new ThreadFactory() {
                    private int count = 1;

                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "business-thread-" + count++);
                    }
                },
                // 拒绝策略(大厂推荐:主线程执行,避免任务丢失)
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    /**
     * IO密集型线程池(如文件上传/下载)
     */
    @Bean("ioThreadPool")
    public Executor ioThreadPool() {
        // IO密集型任务线程数可配置为CPU核心数*4
        int ioCoreSize = Runtime.getRuntime().availableProcessors() * 4;
        int ioMaxSize = Runtime.getRuntime().availableProcessors() * 8;
        return new ThreadPoolExecutor(
                ioCoreSize,
                ioMaxSize,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(2000),
                new ThreadFactory() {
                    private int count = 1;

                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "io-thread-" + count++);
                    }
                },
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
}

2. 核心枚举类

2.1 ErrorCodeEnum(错误码枚举)

java 复制代码
package com.xxx.common.enums;

import lombok.Getter;

/**
 * 全局错误码枚举(大厂规范:分段管理)
 * 常用度:100%
 * 所属层级:通用层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Getter
public enum ErrorCodeEnum {
    // 系统错误(100-199)
    SYSTEM_ERROR(100, "系统异常,请稍后重试"),
    SYSTEM_TIMEOUT(101, "系统超时,请稍后重试"),
    SERVICE_UNAVAILABLE(102, "服务暂不可用"),

    // 认证授权错误(200-299)
    TOKEN_INVALID(200, "Token无效或已过期"),
    TOKEN_MISSING(201, "Token缺失"),
    PERMISSION_DENIED(202, "权限不足"),
    USER_NOT_LOGIN(203, "用户未登录"),

    // 业务错误(600-999)
    BUSINESS_COMMON_ERROR(600, "业务异常"),
    DATA_NOT_FOUND(601, "数据不存在"),
    DATA_DUPLICATE(602, "数据已存在"),
    PARAM_INVALID(603, "参数无效"),

    // 扩展业务错误(按模块细分)
    USER_NOT_EXIST(604, "用户不存在"),
    ORDER_STATUS_ERROR(605, "订单状态异常"),
    INSUFFICIENT_BALANCE(606, "余额不足");

    /**
     * 错误码
     */
    private final int code;

    /**
     * 错误描述
     */
    private final String msg;

    ErrorCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    /**
     * 根据错误码获取枚举
     */
    public static ErrorCodeEnum getByCode(int code) {
        for (ErrorCodeEnum e : values()) {
            if (e.getCode() == code) {
                return e;
            }
        }
        return SYSTEM_ERROR;
    }
}

2.2 HttpStatusEnum(HTTP状态码映射)

java 复制代码
package com.xxx.common.enums;

import lombok.Getter;
import org.springframework.http.HttpStatus;

/**
 * HTTP状态码枚举(与业务错误码关联)
 * 常用度:100%
 * 所属层级:通用层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Getter
public enum HttpStatusEnum {
    SUCCESS(HttpStatus.OK.value(), "成功"),
    BAD_REQUEST(HttpStatus.BAD_REQUEST.value(), "请求参数错误"),
    UNAUTHORIZED(HttpStatus.UNAUTHORIZED.value(), "未认证"),
    FORBIDDEN(HttpStatus.FORBIDDEN.value(), "禁止访问"),
    NOT_FOUND(HttpStatus.NOT_FOUND.value(), "资源不存在"),
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误"),
    TOO_MANY_REQUESTS(HttpStatus.TOO_MANY_REQUESTS.value(), "请求过于频繁");

    /**
     * HTTP状态码
     */
    private final int code;

    /**
     * 描述
     */
    private final String msg;

    HttpStatusEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    /**
     * 根据业务错误码匹配HTTP状态码
     */
    public static HttpStatusEnum matchByBizCode(ErrorCodeEnum bizCode) {
        switch (bizCode) {
            case PARAM_INVALID:
                return BAD_REQUEST;
            case TOKEN_INVALID:
            case TOKEN_MISSING:
            case USER_NOT_LOGIN:
                return UNAUTHORIZED;
            case PERMISSION_DENIED:
                return FORBIDDEN;
            case DATA_NOT_FOUND:
                return NOT_FOUND;
            case SYSTEM_ERROR:
            case SYSTEM_TIMEOUT:
            case SERVICE_UNAVAILABLE:
                return INTERNAL_SERVER_ERROR;
            default:
                return SUCCESS;
        }
    }
}

2.3 BusinessStatusEnum(通用业务状态)

java 复制代码
package com.xxx.common.enums;

import lombok.Getter;

/**
 * 通用业务状态枚举(大厂规范:统一状态值)
 * 常用度:100%
 * 所属层级:业务层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Getter
public enum BusinessStatusEnum {
    // 启用/禁用
    ENABLE(1, "启用"),
    DISABLE(0, "禁用"),

    // 有效/无效
    VALID(1, "有效"),
    INVALID(0, "无效"),

    // 审核状态
    AUDIT_PENDING(0, "待审核"),
    AUDIT_APPROVED(1, "审核通过"),
    AUDIT_REJECTED(2, "审核驳回"),

    // 支付状态
    PAY_PENDING(0, "待支付"),
    PAY_SUCCESS(1, "支付成功"),
    PAY_FAILED(2, "支付失败"),
    PAY_REFUNDED(3, "已退款");

    /**
     * 状态值
     */
    private final int value;

    /**
     * 状态描述
     */
    private final String desc;

    BusinessStatusEnum(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }

    /**
     * 根据值获取枚举
     */
    public static BusinessStatusEnum getByValue(int value) {
        for (BusinessStatusEnum e : values()) {
            if (e.getValue() == value) {
                return e;
            }
        }
        return null;
    }

    /**
     * 判断是否为启用状态
     */
    public boolean isEnable() {
        return this == ENABLE;
    }

    /**
     * 判断是否为有效状态
     */
    public boolean isValid() {
        return this == VALID;
    }
}
```##### 2.4 EnvEnum(环境枚举)
```java
package com.xxx.common.enums;

import lombok.Getter;

/**
 * 环境枚举(避免硬编码环境判断)
 * 常用度:100%
 * 所属层级:通用层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Getter
public enum EnvEnum {
    DEV("dev", "开发环境"),
    TEST("test", "测试环境"),
    PRE("pre", "预发布环境"),
    PROD("prod", "生产环境");

    /**
     * 环境标识
     */
    private final String code;

    /**
     * 环境描述
     */
    private final String desc;

    EnvEnum(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    /**
     * 根据标识获取枚举
     */
    public static EnvEnum getByCode(String code) {
        for (EnvEnum e : values()) {
            if (e.getCode().equals(code)) {
                return e;
            }
        }
        return DEV;
    }

    /**
     * 判断是否为生产环境
     */
    public boolean isProd() {
        return this == PROD;
    }

    /**
     * 判断是否为开发/测试环境
     */
    public boolean isDevOrTest() {
        return this == DEV || this == TEST;
    }
}

3. 基础工具类

3.1 StringUtil(字符串工具)

java 复制代码
package com.xxx.common.util;

import org.apache.commons.lang3.StringUtils;

import java.util.regex.Pattern;

/**
 * 字符串工具类(空判断/脱敏/格式校验,避免NPE)
 * 常用度:100%
 * 所属层级:通用层(控制/业务/DAO)
 *
 * @author xxx
 * @date 2025-11-30
 */
public class StringUtil extends StringUtils {

    /**
     * 手机号正则
     */
    private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");

    /**
     * 身份证正则(18位)
     */
    private static final Pattern ID_CARD_PATTERN = Pattern.compile("^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$");

    /**
     * 手机号脱敏(138****1234)
     */
    public static String maskMobile(String mobile) {
        if (isEmpty(mobile) || !isMobile(mobile)) {
            return mobile;
        }
        return mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }

    /**
     * 身份证脱敏(110101********1234)
     */
    public static String maskIdCard(String idCard) {
        if (isEmpty(idCard) || idCard.length() != 18) {
            return idCard;
        }
        return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
    }

    /**
     * 姓名脱敏(张*三 / 李*)
     */
    public static String maskName(String name) {
        if (isEmpty(name)) {
            return name;
        }
        int length = name.length();
        if (length == 1) {
            return name;
        } else if (length == 2) {
            return name.substring(0, 1) + "*";
        } else {
            return name.substring(0, 1) + "*" + name.substring(length - 1);
        }
    }

    /**
     * 银行卡脱敏(6226****1234)
     */
    public static String maskBankCard(String bankCard) {
        if (isEmpty(bankCard) || bankCard.length() < 8) {
            return bankCard;
        }
        return bankCard.replaceAll("(\\d{4})\\d+(\\d{4})", "$1****$2");
    }

    /**
     * 校验手机号
     */
    public static boolean isMobile(String mobile) {
        if (isEmpty(mobile)) {
            return false;
        }
        return MOBILE_PATTERN.matcher(mobile).matches();
    }

    /**
     * 校验身份证(18位)
     */
    public static boolean isIdCard(String idCard) {
        if (isEmpty(idCard)) {
            return false;
        }
        return ID_CARD_PATTERN.matcher(idCard).matches();
    }

    /**
     * 空判断(兼容空白字符)
     */
    public static boolean isBlank(String str) {
        return str == null || str.trim().isEmpty();
    }

    /**
     * 非空判断
     */
    public static boolean isNotBlank(String str) {
        return !isBlank(str);
    }

    /**
     * 生成随机字符串
     */
    public static String randomStr(int length) {
        if (length <= 0) {
            return "";
        }
        String chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
            sb.append(chars.charAt((int) (Math.random() * chars.length())));
        }
        return sb.toString();
    }
}

3.2 DateUtil(时间工具)

java 复制代码
package com.xxx.common.util;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;

/**
 * 时间工具类(LocalDateTime处理、格式化、时区转换)
 * 常用度:100%
 * 所属层级:通用层
 *
 * @author xxx
 * @date 2025-11-30
 */
public class DateUtil {

    /**
     * 默认时间格式
     */
    public static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss";

    /**
     * 日期格式(无时间)
     */
    public static final String DATE_PATTERN = "yyyy-MM-dd";

    /**
     * 时区(默认东八区)
     */
    private static final ZoneId DEFAULT_ZONE_ID = ZoneId.of("Asia/Shanghai");

    /**
     * LocalDateTime转String
     */
    public static String format(LocalDateTime dateTime) {
        return format(dateTime, DEFAULT_PATTERN);
    }

    /**
     * LocalDateTime转String(自定义格式)
     */
    public static String format(LocalDateTime dateTime, String pattern) {
        if (dateTime == null) {
            return "";
        }
        return DateTimeFormatter.ofPattern(pattern).format(dateTime);
    }

    /**
     * String转LocalDateTime
     */
    public static LocalDateTime parse(String dateStr) {
        return parse(dateStr, DEFAULT_PATTERN);
    }

    /**
     * String转LocalDateTime(自定义格式)
     */
    public static LocalDateTime parse(String dateStr, String pattern) {
        if (StringUtil.isBlank(dateStr)) {
            return null;
        }
        return LocalDateTime.parse(dateStr, DateTimeFormatter.ofPattern(pattern));
    }

    /**
     * Date转LocalDateTime
     */
    public static LocalDateTime dateToLocalDateTime(Date date) {
        if (date == null) {
            return null;
        }
        return date.toInstant().atZone(DEFAULT_ZONE_ID).toLocalDateTime();
    }

    /**
     * LocalDateTime转Date
     */
    public static Date localDateTimeToDate(LocalDateTime dateTime) {
        if (dateTime == null) {
            return null;
        }
        return Date.from(dateTime.atZone(DEFAULT_ZONE_ID).toInstant());
    }

    /**
     * 获取当前时间(东八区)
     */
    public static LocalDateTime now() {
        return LocalDateTime.now(DEFAULT_ZONE_ID);
    }

    /**
     * 计算两个时间的间隔(秒)
     */
    public static long betweenSeconds(LocalDateTime start, LocalDateTime end) {
        if (start == null || end == null) {
            return 0;
        }
        return ChronoUnit.SECONDS.between(start, end);
    }

    /**
     * 计算相对时间(如:5分钟前、1小时前)
     */
    public static String relativeTime(LocalDateTime dateTime) {
        if (dateTime == null) {
            return "";
        }
        LocalDateTime now = now();
        long seconds = betweenSeconds(dateTime, now);
        if (seconds < 60) {
            return seconds + "秒前";
        } else if (seconds < 3600) {
            return seconds / 60 + "分钟前";
        } else if (seconds < 86400) {
            return seconds / 3600 + "小时前";
        } else if (seconds < 2592000) {
            return seconds / 86400 + "天前";
        } else {
            return format(dateTime, DATE_PATTERN);
        }
    }

    /**
     * 获取当天开始时间(00:00:00)
     */
    public static LocalDateTime getDayStart(LocalDateTime dateTime) {
        if (dateTime == null) {
            return null;
        }
        return dateTime.withHour(0).withMinute(0).withSecond(0).withNano(0);
    }

    /**
     * 获取当天结束时间(23:59:59)
     */
    public static LocalDateTime getDayEnd(LocalDateTime dateTime) {
        if (dateTime == null) {
            return null;
        }
        return dateTime.withHour(23).withMinute(59).withSecond(59).withNano(999999999);
    }
}

3.3 EncryptUtil(加密工具)

java 复制代码
package com.xxx.common.security.util;

import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.crypto.symmetric.SM4;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;

/**
 * 加密工具类(AES/SM4/MD5,密钥从配置中心读取)
 * 常用度:100%
 * 所属层级:通用层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Component
public class EncryptUtil {

    /**
     * AES密钥(生产环境从配置中心/密钥管理系统读取)
     */
    @Value("${encrypt.aes.key:xxx1234567890abc}")
    private String aesKey;

    /**
     * SM4密钥(国密算法)
     */
    @Value("${encrypt.sm4.key:xxx1234567890abc}")
    private String sm4Key;

    /**
     * MD5加密
     */
    public static String md5(String content) {
        if (StringUtil.isBlank(content)) {
            return "";
        }
        return SecureUtil.md5(content);
    }

    /**
     * AES加密
     */
    public String aesEncrypt(String content) {
        if (StringUtil.isBlank(content)) {
            return "";
        }
        AES aes = SecureUtil.aes(aesKey.getBytes(StandardCharsets.UTF_8));
        return aes.encryptHex(content);
    }

    /**
     * AES解密
     */
    public String aesDecrypt(String encryptStr) {
        if (StringUtil.isBlank(encryptStr)) {
            return "";
        }
        AES aes = SecureUtil.aes(aesKey.getBytes(StandardCharsets.UTF_8));
        return aes.decryptStr(encryptStr);
    }

    /**
     * SM4加密(国密)
     */
    public String sm4Encrypt(String content) {
        if (StringUtil.isBlank(content)) {
            return "";
        }
        SM4 sm4 = new SM4(sm4Key.getBytes(StandardCharsets.UTF_8));
        return sm4.encryptHex(content);
    }

    /**
     * SM4解密(国密)
     */
    public String sm4Decrypt(String encryptStr) {
        if (StringUtil.isBlank(encryptStr)) {
            return "";
        }
        SM4 sm4 = new SM4(sm4Key.getBytes(StandardCharsets.UTF_8));
        return sm4.decryptStr(encryptStr);
    }

    /**
     * 生成加盐MD5
     */
    public static String md5WithSalt(String content, String salt) {
        if (StringUtil.isBlank(content) || StringUtil.isBlank(salt)) {
            return "";
        }
        return SecureUtil.md5(content + salt);
    }
}

3.4 NumberUtil(数字工具)

java 复制代码
package com.xxx.common.util;

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * 数字工具类(金额处理、精度校验,避免精度丢失)
 * 常用度:100%
 * 所属层级:通用层(业务/控制)
 *
 * @author xxx
 * @date 2025-11-30
 */
public class NumberUtil {

    /**
     * 默认小数位数(金额)
     */
    private static final int DEFAULT_SCALE = 2;

    /**
     * 四舍五入
     */
    private static final RoundingMode DEFAULT_ROUND_MODE = RoundingMode.HALF_UP;

    /**
     * 字符串转BigDecimal(空值返回0)
     */
    public static BigDecimal toBigDecimal(String str) {
        if (StringUtil.isBlank(str)) {
            return BigDecimal.ZERO;
        }
        try {
            return new BigDecimal(str);
        } catch (NumberFormatException e) {
            return BigDecimal.ZERO;
        }
    }

    /**
     * 数字转BigDecimal(空值返回0)
     */
    public static BigDecimal toBigDecimal(Number num) {
        if (num == null) {
            return BigDecimal.ZERO;
        }
        return new BigDecimal(num.toString());
    }

    /**
     * 金额加法(保留2位小数)
     */
    public static BigDecimal add(BigDecimal num1, BigDecimal num2) {
        return num1.add(num2).setScale(DEFAULT_SCALE, DEFAULT_ROUND_MODE);
    }

    /**
     * 金额减法(保留2位小数)
     */
    public static BigDecimal subtract(BigDecimal num1, BigDecimal num2) {
        return num1.subtract(num2).setScale(DEFAULT_SCALE, DEFAULT_ROUND_MODE);
    }

    /**
     * 金额乘法(保留2位小数)
     */
    public static BigDecimal multiply(BigDecimal num1, BigDecimal num2) {
        return num1.multiply(num2).setScale(DEFAULT_SCALE, DEFAULT_ROUND_MODE);
    }

    /**
     * 金额除法(保留2位小数)
     */
    public static BigDecimal divide(BigDecimal num1, BigDecimal num2) {
        if (num2.compareTo(BigDecimal.ZERO) == 0) {
            return BigDecimal.ZERO;
        }
        return num1.divide(num2, DEFAULT_SCALE, DEFAULT_ROUND_MODE);
    }

    /**
     * 元转分(避免浮点数问题)
     */
    public static long yuanToFen(BigDecimal yuan) {
        return multiply(yuan, new BigDecimal("100")).longValue();
    }

    /**
     * 分转元(保留2位小数)
     */
    public static BigDecimal fenToYuan(long fen) {
        return divide(new BigDecimal(fen), new BigDecimal("100"));
    }

    /**
     * 判断是否为正数
     */
    public static boolean isPositive(BigDecimal num) {
        return num.compareTo(BigDecimal.ZERO) > 0;
    }

    /**
     * 判断是否为非负数
     */
    public static boolean isNonNegative(BigDecimal num) {
        return num.compareTo(BigDecimal.ZERO) >= 0;
    }

    /**
     * 数字格式化(保留指定小数位)
     */
    public static String format(BigDecimal num, int scale) {
        return num.setScale(scale, DEFAULT_ROUND_MODE).toString();
    }
}

3.5 CollectionUtil(集合工具)

java 复制代码
package com.xxx.common.util;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 集合工具类(空判断、列表转树、去重)
 * 常用度:100%
 * 所属层级:通用层
 *
 * @author xxx
 * @date 2025-11-30
 */
public class CollectionUtil extends CollectionUtils {

    /**
     * 空判断
     */
    public static boolean isEmpty(Collection<?> coll) {
        return coll == null || coll.isEmpty();
    }

    /**
     * 非空判断
     */
    public static boolean isNotEmpty(Collection<?> coll) {
        return !isEmpty(coll);
    }

    /**
     * Map空判断
     */
    public static boolean isEmpty(Map<?, ?> map) {
        return MapUtils.isEmpty(map);
    }

    /**
     * Map非空判断
     */
    public static boolean isNotEmpty(Map<?, ?> map) {
        return MapUtils.isNotEmpty(map);
    }

    /**
     * 列表去重(根据指定字段)
     */
    public static <T, K> List<T> distinctByField(List<T> list, Function<T, K> keyExtractor) {
        if (isEmpty(list)) {
            return new ArrayList<>();
        }
        Map<K, T> map = new LinkedHashMap<>();
        for (T t : list) {
            K key = keyExtractor.apply(t);
            if (key != null && !map.containsKey(key)) {
                map.put(key, t);
            }
        }
        return new ArrayList<>(map.values());
    }

    /**
     * 列表转树结构(通用)
     *
     * @param list      源列表
     * @param idGetter  ID获取器
     * @param parentIdGetter 父ID获取器
     * @param childrenSetter 子节点设置器
     * @param <T>       节点类型
     * @param <ID>      ID类型
     * @return 树结构列表
     */
    public static <T, ID> List<T> listToTree(List<T> list,
                                             Function<T, ID> idGetter,
                                             Function<T, ID> parentIdGetter,
                                             java.util.function.BiConsumer<T, List<T>> childrenSetter) {
        if (isEmpty(list)) {
            return new ArrayList<>();
        }

        // 构建ID->节点映射
        Map<ID, T> idNodeMap = list.stream()
                .collect(Collectors.toMap(idGetter, t -> t, (t1, t2) -> t1));

        // 构建树
        List<T> rootList = new ArrayList<>();
        for (T node : list) {
            ID parentId = parentIdGetter.apply(node);
            if (parentId == null || !idNodeMap.containsKey(parentId)) {
                // 根节点
                rootList.add(node);
            } else {
                // 子节点
                T parentNode = idNodeMap.get(parentId);
                // 获取父节点的子列表
                List<T> children = new ArrayList<>();
                // 反射/自定义方式获取子列表(此处简化,实际可通过BeanUtil获取)
                childrenSetter.accept(parentNode, children);
                children.add(node);
            }
        }
        return rootList;
    }

    /**
     * 列表转Map(指定Key,值为单个对象)
     */
    public static <T, K> Map<K, T> listToMap(List<T> list, Function<T, K> keyExtractor) {
        if (isEmpty(list)) {
            return new HashMap<>();
        }
        return list.stream()
                .collect(Collectors.toMap(keyExtractor, t -> t, (t1, t2) -> t1));
    }

    /**
     * 列表转Map(指定Key,值为列表)
     */
    public static <T, K> Map<K, List<T>> listToMapList(List<T> list, Function<T, K> keyExtractor) {
        if (isEmpty(list)) {
            return new HashMap<>();
        }
        return list.stream()
                .collect(Collectors.groupingBy(keyExtractor));
    }
}

3.6 BeanUtil(对象转换)

java 复制代码
package com.xxx.common.util;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

import java.beans.PropertyDescriptor;
import java.util.HashSet;
import java.util.Set;

/**
 * 对象转换工具类(深拷贝、Bean转Map,基于Spring/CommonsBeanutils)
 * 常用度:100%
 * 所属层级:通用层
 *
 * @author xxx
 * @date 2025-11-30
 */
public class BeanUtil extends org.apache.commons.beanutils.BeanUtils {

    /**
     * 浅拷贝(忽略空值)
     */
    public static void copyPropertiesIgnoreNull(Object source, Object target) {
        BeanUtils.copyProperties(source, target, getNullPropertyNames(source));
    }

    /**
     * 深拷贝(创建新对象)
     */
    public static <T> T deepCopy(Object source, Class<T> targetClass) {
        if (source == null) {
            return null;
        }
        try {
            T target = targetClass.getDeclaredConstructor().newInstance();
            copyPropertiesIgnoreNull(source, target);
            return target;
        } catch (Exception e) {
            throw new RuntimeException("Bean深拷贝失败", e);
        }
    }

    /**
     * 获取空值属性名
     */
    private static String[] getNullPropertyNames(Object source) {
        final BeanWrapper src = new BeanWrapperImpl(source);
        PropertyDescriptor[] pds = src.getPropertyDescriptors();

        Set<String> emptyNames = new HashSet<>();
        for (PropertyDescriptor pd : pds) {
            Object srcValue = src.getPropertyValue(pd.getName());
            if (srcValue == null) {
                emptyNames.add(pd.getName());
            }
        }
        return emptyNames.toArray(new String[0]);
    }

    /**
     * Bean转Map(忽略空值)
     */
    public static java.util.Map<String, Object> beanToMap(Object bean) {
        if (bean == null) {
            return new java.util.HashMap<>();
        }
        java.util.Map<String, Object> map = new java.util.HashMap<>();
        try {
            PropertyDescriptor[] pds = new BeanWrapperImpl(bean).getPropertyDescriptors();
            for (PropertyDescriptor pd : pds) {
                String name = pd.getName();
                if (!"class".equals(name)) {
                    Object value = pd.getReadMethod().invoke(bean);
                    if (value != null) {
                        map.put(name, value);
                    }
                }
            }
            return map;
        } catch (Exception e) {
            throw new RuntimeException("Bean转Map失败", e);
        }
    }

    /**
     * Map转Bean
     */
    public static <T> T mapToBean(java.util.Map<String, Object> map, Class<T> beanClass) {
        if (CollectionUtil.isEmpty(map)) {
            return null;
        }
        try {
            T bean = beanClass.getDeclaredConstructor().newInstance();
            org.apache.commons.beanutils.BeanUtils.populate(bean, map);
            return bean;
        } catch (Exception e) {
            throw new RuntimeException("Map转Bean失败", e);
        }
    }
}

二、接口交互模块(接口开发必用,100%通用)

1. 统一返回相关

1.1 ApiResponse(全局返回体)

java 复制代码
package com.xxx.common.web.response;

import com.xxx.common.enums.ApiResultCode;
import com.xxx.common.enums.HttpStatusEnum;
import lombok.Data;

import java.io.Serializable;

/**
 * 全局统一返回体(含code/msg/data/httpCode/traceId)
 * 常用度:100%
 * 所属层级:控制层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Data
public class ApiResponse<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 业务返回码
     */
    private int code;

    /**
     * 返回消息
     */
    private String msg;

    /**
     * 返回数据
     */
    private T data;

    /**
     * HTTP状态码
     */
    private int httpCode;

    /**
     * 全链路TraceId
     */
    private String traceId;

    /**
     * 成功返回(无数据)
     */
    public static <T> ApiResponse<T> success() {
        return success(null);
    }

    /**
     * 成功返回(带数据)
     */
    public static <T> ApiResponse<T> success(T data) {
        ApiResultCode successCode = ApiResultCode.SUCCESS;
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(successCode.getCode());
        response.setMsg(successCode.getMsg());
        response.setData(data);
        response.setHttpCode(HttpStatusEnum.SUCCESS.getCode());
        response.setTraceId(TraceIdUtil.getTraceId()); // 后续实现TraceIdUtil
        return response;
    }

    /**
     * 失败返回(自定义业务码)
     */
    public static <T> ApiResponse<T> fail(ApiResultCode resultCode) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(resultCode.getCode());
        response.setMsg(resultCode.getMsg());
        response.setHttpCode(HttpStatusEnum.matchByBizCode(resultCode.getBizCode()).getCode());
        response.setTraceId(TraceIdUtil.getTraceId());
        return response;
    }

    /**
     * 失败返回(自定义消息)
     */
    public static <T> ApiResponse<T> fail(int code, String msg) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(code);
        response.setMsg(msg);
        response.setHttpCode(HttpStatusEnum.BAD_REQUEST.getCode());
        response.setTraceId(TraceIdUtil.getTraceId());
        return response;
    }
}

1.2 PageParam(分页请求参数)

java 复制代码
package com.xxx.common.web.param;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.Data;

import java.io.Serializable;

/**
 * 分页请求参数(页码/页大小校验,支持排序)
 * 常用度:100%
 * 所属层级:控制层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Data
public class PageParam implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 当前页码(默认1)
     */
    @Min(value = 1, message = "页码不能小于1")
    private Integer pageNum = 1;

    /**
     * 页大小(默认10,最大100)
     */
    @Min(value = 1, message = "页大小不能小于1")
    @Max(value = 100, message = "页大小不能超过100")
    private Integer pageSize = 10;

    /**
     * 排序字段(如:createTime)
     */
    private String sortField;

    /**
     * 排序方式(asc/desc,默认desc)
     */
    private String sortType = "desc";

    /**
     * 获取MyBatisPlus分页对象
     */
    public com.baomidou.mybatisplus.extension.plugins.pagination.Page<?> toPage() {
        return new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(pageNum, pageSize);
    }
}

1.3 PageResult(分页结果)

java 复制代码
package com.xxx.common.web.response;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;

import java.io.Serializable;
import java.util.List;

/**
 * 分页结果(兼容MyBatisPlus IPage)
 * 常用度:100%
 * 所属层级:控制层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Data
public class PageResult<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 当前页码
     */
    private Integer pageNum;

    /**
     * 页大小
     */
    private Integer pageSize;

    /**
     * 总条数
     */
    private Long total;

    /**
     * 总页数
     */
    private Integer pages;

    /**
     * 数据列表
     */
    private List<T> list;

    /**
     * 从MyBatisPlus Page转换
     */
    public static <T> PageResult<T> from(Page<T> page) {
        PageResult<T> result = new PageResult<>();
        result.setPageNum((int) page.getCurrent());
        result.setPageSize((int) page.getSize());
        result.setTotal(page.getTotal());
        result.setPages((int) page.getPages());
        result.setList(page.getRecords());
        return result;
    }

    /**
     * 自定义分页结果
     */
    public static <T> PageResult<T> of(Integer pageNum, Integer pageSize, Long total, List<T> list) {
        PageResult<T> result = new PageResult<>();
        result.setPageNum(pageNum);
        result.setPageSize(pageSize);
        result.setTotal(total);
        result.setPages(total % pageSize == 0 ? (int) (total / pageSize) : (int) (total / pageSize) + 1);
        result.setList(list);
        return result;
    }
}

1.4 ApiResultCode(返回码枚举)

java 复制代码
package com.xxx.common.enums;

import lombok.Getter;

/**
 * 接口返回码枚举(与HTTP状态码绑定)
 * 常用度:100%
 * 所属层级:通用层(控制/业务)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Getter
public enum ApiResultCode {
    /**
     * 成功
     */
    SUCCESS(20000, "操作成功", ErrorCodeEnum.SYSTEM_ERROR),

    /**
     * 失败
     */
    FAIL(40000, "操作失败", ErrorCodeEnum.BUSINESS_COMMON_ERROR),

    /**
     * 参数无效
     */
    PARAM_INVALID(40001, "参数无效", ErrorCodeEnum.PARAM_INVALID),

    /**
     * 未认证
     */
    UNAUTHORIZED(40100, "未认证", ErrorCodeEnum.TOKEN_INVALID),

    /**
     * 权限不足
     */
    FORBIDDEN(40300, "权限不足", ErrorCodeEnum.PERMISSION_DENIED),

    /**
     * 资源不存在
     */
    NOT_FOUND(40400, "资源不存在", ErrorCodeEnum.DATA_NOT_FOUND),

    /**
     * 系统异常
     */
    SYSTEM_ERROR(50000, "系统异常", ErrorCodeEnum.SYSTEM_ERROR),

    /**
     * 请求过于频繁
     */
    TOO_MANY_REQUESTS(42900, "请求过于频繁", ErrorCodeEnum.SYSTEM_ERROR);

    /**
     * 返回码
     */
    private final int code;

    /**
     * 返回消息
     */
    private final String msg;

    /**
     * 关联业务错误码
     */
    private final ErrorCodeEnum bizCode;

    ApiResultCode(int code, String msg, ErrorCodeEnum bizCode) {
        this.code = code;
        this.msg = msg;
        this.bizCode = bizCode;
    }
}

2. 参数校验相关

2.1 GroupValidator(分组校验接口)

java 复制代码
package com.xxx.common.web.validator.group;

/**
 * 参数校验分组(区分新增/更新场景)
 * 常用度:100%
 * 所属层级:控制层
 *
 * @author xxx
 * @date 2025-11-30
 */
public interface GroupValidator {

    /**
     * 新增分组
     */
    interface AddGroup {}

    /**
     * 更新分组
     */
    interface UpdateGroup {}

    /**
     * 删除分组
     */
    interface DeleteGroup {}

    /**
     * 查询分组
     */
    interface QueryGroup {}
}

2.2 CustomValidAnnotations(自定义校验注解)

java 复制代码
package com.xxx.common.web.validator.annotation;

import com.xxx.common.web.validator.ConstraintValidators.MobileValidator;
import com.xxx.common.web.validator.ConstraintValidators.IdCardValidator;
import com.xxx.common.web.validator.ConstraintValidators.NotBlankOrNullValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.*;

/**
 * 自定义校验注解(@Mobile/@IdCard/@NotBlankOrNull)
 * 常用度:100%
 * 所属层级:控制层
 *
 * @author xxx
 * @date 2025-11-30
 */
public class CustomValidAnnotations {

    /**
     * 手机号校验注解
     */
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Constraint(validatedBy = MobileValidator.class)
    public @interface Mobile {
        String message() default "手机号格式不正确";

        Class<?>[] groups() default {};

        Class<? extends Payload>[] payload() default {};
    }

    /**
     * 身份证校验注解
     */
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Constraint(validatedBy = IdCardValidator.class)
    public @interface IdCard {
        String message() default "身份证格式不正确";

        Class<?>[] groups() default {};

        Class<? extends Payload>[] payload() default {};
    }

    /**
     * 非空或null校验(允许空字符串,但不允许null)
     */
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Constraint(validatedBy = NotBlankOrNullValidator.class)
    public @interface NotBlankOrNull {
        String message() default "字段不能为空字符串";

        Class<?>[] groups() default {};

        Class<? extends Payload>[] payload() default {};
    }
}

// 校验器实现类(单独存放)
package com.xxx.common.web.validator.ConstraintValidators;

import com.xxx.common.util.StringUtil;
import com.xxx.common.web.validator.annotation.CustomValidAnnotations;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

/**
 * 手机号校验器
 */
public class MobileValidator implements ConstraintValidator<CustomValidAnnotations.Mobile, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 允许null(如需非空,配合@NotNull使用)
        if (StringUtil.isBlank(value)) {
            return true;
        }
        return StringUtil.isMobile(value);
    }
}

/**
 * 身份证校验器
 */
public class IdCardValidator implements ConstraintValidator<CustomValidAnnotations.IdCard, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (StringUtil.isBlank(value)) {
            return true;
        }
        return StringUtil.isIdCard(value);
    }
}

/**
 * 非空或null校验器
 */
public class NotBlankOrNullValidator implements ConstraintValidator<CustomValidAnnotations.NotBlankOrNull, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 允许空字符串,但不允许null
        return value != null;
    }
}

2.3 ValidUtil(手动校验工具)

java 复制代码
package com.xxx.common.web.validator;

import com.xxx.common.exception.ParamInvalidException;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;

import java.util.Set;
import java.util.stream.Collectors;

/**
 * 参数手动校验工具(抛ParamInvalidException)
 * 常用度:100%
 * 所属层级:控制层/业务层
 *
 * @author xxx
 * @date 2025-11-30
 */
public class ValidUtil {

    private static final Validator VALIDATOR;

    static {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        VALIDATOR = factory.getValidator();
    }

    /**
     * 手动校验对象(默认分组)
     */
    public static <T> void validate(T obj) {
        validate(obj, new Class<?>[]{});
    }

    /**
     * 手动校验对象(指定分组)
     */
    public static <T> void validate(T obj, Class<?>... groups) {
        if (obj == null) {
            throw new ParamInvalidException("校验对象不能为空");
        }
        Set<ConstraintViolation<T>> violations = VALIDATOR.validate(obj, groups);
        if (!violations.isEmpty()) {
            // 拼接错误信息(如:username不能为空, mobile格式不正确)
            String errorMsg = violations.stream()
                    .map(ConstraintViolation::getMessage)
                    .collect(Collectors.joining(", "));
            throw new ParamInvalidException(errorMsg);
        }
    }

    /**
     * 校验单个字段
     */
    public static <T> void validateField(T obj, String fieldName) {
        Set<ConstraintViolation<T>> violations = VALIDATOR.validateProperty(obj, fieldName);
        if (!violations.isEmpty()) {
            String errorMsg = violations.stream()
                    .map(ConstraintViolation::getMessage)
                    .collect(Collectors.joining(", "));
            throw new ParamInvalidException(fieldName + ":" + errorMsg);
        }
    }
}

三、业务逻辑模块(业务开发必用,95%通用)

1. 异常处理

1.1 GlobalExceptionHandler(全局异常拦截)

java 复制代码
package com.xxx.common.web.exception;

import com.xxx.common.enums.ApiResultCode;
import com.xxx.common.enums.EnvEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.exception.ParamInvalidException;
import com.xxx.common.exception.SystemException;
import com.xxx.common.monitor.util.ExceptionLogUtil;
import com.xxx.common.util.StringUtil;
import com.xxx.common.web.response.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import java.util.List;
import java.util.stream.Collectors;

/**
 * 全局异常处理器(区分业务/系统/参数异常,生产环境隐藏堆栈)
 * 常用度:100%
 * 所属层级:控制层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 环境标识(从配置文件读取)
     */
    @Value("${spring.profiles.active:dev}")
    private String env;

    /**
     * 业务异常处理
     */
    @ExceptionHandler(BusinessException.class)
    public ApiResponse<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
        // 记录业务异常日志(级别为WARN)
        log.warn("[业务异常] URI: {}, 错误码: {}, 消息: {}",
                request.getRequestURI(), e.getErrorCode().getCode(), e.getMessage());
        // 记录异常审计日志
        ExceptionLogUtil.recordBusinessException(e, request);
        return ApiResponse.fail(ApiResultCode.FAIL.getCode(), e.getMessage());
    }

    /**
     * 参数异常处理
     */
    @ExceptionHandler(ParamInvalidException.class)
    public ApiResponse<Void> handleParamInvalidException(ParamInvalidException e, HttpServletRequest request) {
        log.warn("[参数异常] URI: {}, 错误信息: {}", request.getRequestURI(), e.getMessage());
        ExceptionLogUtil.recordParamException(e, request);
        return ApiResponse.fail(ApiResultCode.PARAM_INVALID.getCode(), e.getMessage());
    }

    /**
     * 系统异常处理
     */
    @ExceptionHandler(SystemException.class)
    public ApiResponse<Void> handleSystemException(SystemException e, HttpServletRequest request) {
        // 系统异常记录ERROR级别日志
        log.error("[系统异常] URI: {}", request.getRequestURI(), e);
        ExceptionLogUtil.recordSystemException(e, request);
        // 生产环境隐藏具体异常信息
        String msg = EnvEnum.getByCode(env).isProd() ? ApiResultCode.SYSTEM_ERROR.getMsg() : e.getMessage();
        return ApiResponse.fail(ApiResultCode.SYSTEM_ERROR.getCode(), msg);
    }

    /**
     * 接口参数校验异常(@Valid)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        String errorMsg = fieldErrors.stream()
                .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage()))
                .collect(Collectors.joining("; "));
        log.warn("[参数校验异常] URI: {}, 错误信息: {}", request.getRequestURI(), errorMsg);
        return ApiResponse.fail(ApiResultCode.PARAM_INVALID.getCode(), errorMsg);
    }

    /**
     * 请求参数绑定异常
     */
    @ExceptionHandler(BindException.class)
    public ApiResponse<Void> handleBindException(BindException e, HttpServletRequest request) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        String errorMsg = fieldErrors.stream()
                .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage()))
                .collect(Collectors.joining("; "));
        log.warn("[参数绑定异常] URI: {}, 错误信息: {}", request.getRequestURI(), errorMsg);
        return ApiResponse.fail(ApiResultCode.PARAM_INVALID.getCode(), errorMsg);
    }

    /**
     * 请求体解析异常(如JSON格式错误)
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ApiResponse<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
        log.warn("[请求体解析异常] URI: {}, 错误信息: {}", request.getRequestURI(), e.getMessage());
        String msg = "请求体格式错误,请检查JSON格式";
        return ApiResponse.fail(ApiResultCode.PARAM_INVALID.getCode(), msg);
    }

    /**
     * 404异常
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public ApiResponse<Void> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
        log.warn("[404异常] URI: {}, 错误信息: {}", request.getRequestURI(), e.getMessage());
        return ApiResponse.fail(ApiResultCode.NOT_FOUND.getCode(), "接口不存在");
    }

    /**
     * 兜底异常处理
     */
    @ExceptionHandler(Exception.class)
    public ApiResponse<Void> handleException(Exception e, HttpServletRequest request) {
        log.error("[未知异常] URI: {}", request.getRequestURI(), e);
        ExceptionLogUtil.recordUnknownException(e, request);
        String msg = EnvEnum.getByCode(env).isProd() ? ApiResultCode.SYSTEM_ERROR.getMsg() : e.getMessage();
        return ApiResponse.fail(ApiResultCode.SYSTEM_ERROR.getCode(), msg);
    }
}

1.2 BusinessException(业务异常)

java 复制代码
package com.xxx.common.exception;

import com.xxx.common.enums.ErrorCodeEnum;
import lombok.Getter;

/**
 * 业务异常(携带错误码,支持自定义消息)
 * 常用度:100%
 * 所属层级:业务层/控制层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Getter
public class BusinessException extends RuntimeException {

    /**
     * 业务错误码
     */
    private final ErrorCodeEnum errorCode;

    /**
     * 构造器(默认错误码)
     */
    public BusinessException(String message) {
        super(message);
        this.errorCode = ErrorCodeEnum.BUSINESS_COMMON_ERROR;
    }

    /**
     * 构造器(自定义错误码)
     */
    public BusinessException(ErrorCodeEnum errorCode) {
        super(errorCode.getMsg());
        this.errorCode = errorCode;
    }

    /**
     * 构造器(自定义错误码+消息)
     */
    public BusinessException(ErrorCodeEnum errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    /**
     * 构造器(自定义消息+异常)
     */
    public BusinessException(String message, Throwable cause) {
        super(message, cause);
        this.errorCode = ErrorCodeEnum.BUSINESS_COMMON_ERROR;
    }
}

1.3 SystemException(系统异常)

java 复制代码
package com.xxx.common.exception;

import com.xxx.common.enums.ErrorCodeEnum;
import lombok.Getter;

/**
 * 系统异常(携带原始异常,便于排查)
 * 常用度:100%
 * 所属层级:通用层(业务/DAO/控制)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Getter
public class SystemException extends RuntimeException {

    /**
     * 系统错误码
     */
    private final ErrorCodeEnum errorCode;

    /**
     * 构造器(默认错误码)
     */
    public SystemException(String message) {
        super(message);
        this.errorCode = ErrorCodeEnum.SYSTEM_ERROR;
    }

    /**
     * 构造器(自定义错误码)
     */
    public SystemException(ErrorCodeEnum errorCode) {
        super(errorCode.getMsg());
        this.errorCode = errorCode;
    }

    /**
     * 构造器(原始异常+默认错误码)
     */
    public SystemException(Throwable cause) {
        super(cause);
        this.errorCode = ErrorCodeEnum.SYSTEM_ERROR;
    }

    /**
     * 构造器(自定义消息+原始异常)
     */
    public SystemException(String message, Throwable cause) {
        super(message, cause);
        this.errorCode = ErrorCodeEnum.SYSTEM_ERROR;
    }

    /**
     * 构造器(自定义错误码+原始异常)
     */
    public SystemException(ErrorCodeEnum errorCode, Throwable cause) {
        super(errorCode.getMsg(), cause);
        this.errorCode = errorCode;
    }
}

1.4 ParamInvalidException(参数异常)

java 复制代码
package com.xxx.common.exception;

import com.xxx.common.enums.ErrorCodeEnum;
import lombok.Getter;

import java.util.List;

/**
 * 参数异常(返回错误字段列表)
 * 常用度:100%
 * 所属层级:控制层/业务层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Getter
public class ParamInvalidException extends RuntimeException {

    /**
     * 错误字段列表
     */
    private final List<String> errorFields;

    /**
     * 构造器(单条错误信息)
     */
    public ParamInvalidException(String message) {
        super(message);
        this.errorFields = List.of(message);
    }

    /**
     * 构造器(错误字段列表)
     */
    public ParamInvalidException(List<String> errorFields) {
        super(String.join("; ", errorFields));
        this.errorFields = errorFields;
    }

    /**
     * 构造器(字段名+错误信息)
     */
    public ParamInvalidException(String fieldName, String message) {
        super(String.format("%s: %s", fieldName, message));
        this.errorFields = List.of(super.getMessage());
    }

    /**
     * 获取错误码
     */
    public ErrorCodeEnum getErrorCode() {
        return ErrorCodeEnum.PARAM_INVALID;
    }
}

2. 业务工具类

2.1 ExcelUtil(EasyExcel导入导出)

java 复制代码
package com.xxx.common.business.util;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.enums.ErrorCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

/**
 * Excel工具类(EasyExcel封装,大文件分片、数据校验)
 * 常用度:95%
 * 所属层级:业务层/控制层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
public class ExcelUtil {

    /**
     * Excel导出(单sheet)
     */
    public static <T> void export(HttpServletResponse response,
                                  String fileName,
                                  String sheetName,
                                  Class<T> clazz,
                                  List<T> data) {
        try {
            // 设置响应头
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            // 防止中文文件名乱码
            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");
            response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");

            // 写入Excel
            EasyExcel.write(response.getOutputStream(), clazz)
                    .excelType(ExcelTypeEnum.XLSX)
                    .sheet(sheetName)
                    .doWrite(data);
        } catch (IOException e) {
            log.error("Excel导出失败", e);
            throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "Excel导出失败:" + e.getMessage());
        }
    }

    /**
     * Excel导入(小文件,一次性读取)
     */
    public static <T> List<T> importExcel(MultipartFile file, Class<T> clazz) {
        if (file == null || file.isEmpty()) {
            throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "导入文件不能为空");
        }
        // 校验文件类型
        String originalFilename = file.getOriginalFilename();
        if (StringUtils.isBlank(originalFilename) ||
                (!originalFilename.endsWith(".xlsx") && !originalFilename.endsWith(".xls"))) {
            throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "仅支持.xlsx/.xls格式文件");
        }

        List<T> dataList = new ArrayList<>();
        try {
            EasyExcel.read(file.getInputStream(), clazz, new AnalysisEventListener<T>() {
                @Override
                public void invoke(T data, AnalysisContext context) {
                    dataList.add(data);
                }

                @Override
                public void doAfterAllAnalysed(AnalysisContext context) {
                    log.info("Excel导入完成,共读取{}条数据", dataList.size());
                }

                @Override
                public void onException(Exception exception, AnalysisContext context) throws Exception {
                    log.error("Excel解析异常,行号:{}", context.readRowHolder().getRowIndex() + 1, exception);
                    throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR,
                            String.format("Excel解析失败,行号:%d,原因:%s",
                                    context.readRowHolder().getRowIndex() + 1, exception.getMessage()));
                }
            }).excelType(ExcelTypeEnum.valueOf(originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toUpperCase()))
                    .sheet()
                    .doRead();
            return dataList;
        } catch (IOException e) {
            log.error("Excel导入失败", e);
            throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "Excel导入失败:" + e.getMessage());
        }
    }

    /**
     * Excel导入(大文件,分片处理)
     */
    public static <T> void importLargeExcel(MultipartFile file,
                                            Class<T> clazz,
                                            int batchSize,
                                            Consumer<List<T>> batchConsumer) {
        if (file == null || file.isEmpty()) {
            throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "导入文件不能为空");
        }
        String originalFilename = file.getOriginalFilename();
        if (StringUtils.isBlank(originalFilename) ||
                (!originalFilename.endsWith(".xlsx") && !originalFilename.endsWith(".xls"))) {
            throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "仅支持.xlsx/.xls格式文件");
        }

        try {
            EasyExcel.read(file.getInputStream(), clazz, new AnalysisEventListener<T>() {
                private List<T> batchData = new ArrayList<>(batchSize);

                @Override
                public void invoke(T data, AnalysisContext context) {
                    batchData.add(data);
                    // 达到批次大小,处理数据
                    if (batchData.size() >= batchSize) {
                        batchConsumer.accept(batchData);
                        batchData.clear();
                    }
                }

                @Override
                public void doAfterAllAnalysed(AnalysisContext context) {
                    // 处理剩余数据
                    if (!batchData.isEmpty()) {
                        batchConsumer.accept(batchData);
                    }
                    log.info("大文件Excel导入完成,总行数:{}", context.readRowHolder().getRowIndex());
                }

                @Override
                public void onException(Exception exception, AnalysisContext context) throws Exception {
                    log.error("大文件Excel解析异常,行号:{}", context.readRowHolder().getRowIndex() + 1, exception);
                    throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR,
                            String.format("Excel解析失败,行号:%d,原因:%s",
                                    context.readRowHolder().getRowIndex() + 1, exception.getMessage()));
                }
            }).excelType(ExcelTypeEnum.valueOf(originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toUpperCase()))
                    .sheet()
                    .doRead();
        } catch (IOException e) {
            log.error("大文件Excel导入失败", e);
            throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "大文件Excel导入失败:" + e.getMessage());
        }
    }
}

2.2 IdGeneratorUtil(分布式ID生成)

java 复制代码
package com.xxx.common.business.util;

import com.xxx.common.exception.SystemException;
import com.xxx.common.enums.ErrorCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 分布式ID生成工具(雪花算法,兼容数据库自增ID)
 * 常用度:95%
 * 所属层级:通用层(业务/DAO)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Component
public class IdGeneratorUtil {

    /**
     * 雪花算法参数
     */
    private static final long TWEPOCH = 1609459200000L; // 2021-01-01 00:00:00
    private static final long WORKER_ID_BITS = 5L;
    private static final long DATACENTER_ID_BITS = 5L;
    private static final long MAX_WORKER_ID = (1L << WORKER_ID_BITS) - 1;
    private static final long MAX_DATACENTER_ID = (1L << DATACENTER_ID_BITS) - 1;
    private static final long SEQUENCE_BITS = 12L;
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
    private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
    private static final long SEQUENCE_MASK = (1L << SEQUENCE_BITS) - 1;

    /**
     * 工作节点ID(从配置文件读取,默认自动生成)
     */
    @Value("${id-generator.worker-id:-1}")
    private long workerId;

    /**
     * 数据中心ID(从配置文件读取,默认自动生成)
     */
    @Value("${id-generator.datacenter-id:-1}")
    private long datacenterId;

    private long sequence = 0L;
    private long lastTimestamp = -1L;
    private final AtomicLong atomicSequence = new AtomicLong(0);

    /**
     * 初始化工作节点ID和数据中心ID
     */
    @PostConstruct
    public void init() {
        if (workerId == -1) {
            workerId = generateWorkerId();
        }
        if (datacenterId == -1) {
            datacenterId = generateDatacenterId();
        }

        if (workerId > MAX_WORKER_ID || workerId < 0) {
            throw new SystemException(ErrorCodeEnum.SYSTEM_ERROR,
                    String.format("Worker ID不能大于%d或小于0", MAX_WORKER_ID));
        }
        if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
            throw new SystemException(ErrorCodeEnum.SYSTEM_ERROR,
                    String.format("Datacenter ID不能大于%d或小于0", MAX_DATACENTER_ID));
        }
        log.info("雪花算法初始化完成,workerId={}, datacenterId={}", workerId, datacenterId);
    }

    /**
     * 生成雪花算法ID
     */
    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();

        // 时钟回拨处理
        if (timestamp < lastTimestamp) {
            log.error("时钟回拨,lastTimestamp={}, currentTimestamp={}", lastTimestamp, timestamp);
            // 等待时钟追上,最多等待3秒
            long waitTime = lastTimestamp - timestamp;
            if (waitTime > 3000) {
                throw new SystemException(ErrorCodeEnum.SYSTEM_ERROR, "时钟回拨超过3秒,无法生成ID");
            }
            try {
                Thread.sleep(waitTime);
                timestamp = System.currentTimeMillis();
                if (timestamp < lastTimestamp) {
                    throw new SystemException(ErrorCodeEnum.SYSTEM_ERROR, "时钟回拨,生成ID失败");
                }
            } catch (InterruptedException e) {
                throw new SystemException(e);
            }
        }

        // 同一毫秒内,序列号递增
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                // 序列号溢出,等待下一毫秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }

        lastTimestamp = timestamp;

        // 组装ID
        return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
                | (datacenterId << DATACENTER_ID_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }

    /**
     * 生成数据库自增风格ID(纯数字,递增)
     */
    public long nextIncrementId() {
        return System.currentTimeMillis() * 10000 + atomicSequence.incrementAndGet() % 10000;
    }

    /**
     * 生成UUID(无横线)
     */
    public String nextUuid() {
        return java.util.UUID.randomUUID().toString().replace("-", "");
    }

    /**
     * 等待下一毫秒
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }

    /**
     * 自动生成工作节点ID(基于MAC地址+进程ID)
     */
    private long generateWorkerId() {
        try {
            // 获取MAC地址
            InetAddress address = InetAddress.getLocalHost();
            NetworkInterface ni = NetworkInterface.getByInetAddress(address);
            byte[] mac = ni.getHardwareAddress();
            long macHash = 0;
            for (byte b : mac) {
                macHash = (macHash << 8) | (b & 0xff);
            }
            // 获取进程ID
            String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
            long pidHash = Long.parseLong(pid);
            // 计算workerId
            return (macHash ^ pidHash) % MAX_WORKER_ID;
        } catch (Exception e) {
            log.warn("自动生成workerId失败,使用随机数", e);
            return (long) (Math.random() * MAX_WORKER_ID);
        }
    }

    /**
     * 自动生成数据中心ID(基于IP地址)
     */
    private long generateDatacenterId() {
        try {
            InetAddress address = InetAddress.getLocalHost();
            byte[] ip = address.getAddress();
            long ipHash = 0;
            for (byte b : ip) {
                ipHash = (ipHash << 8) | (b & 0xff);
            }
            return ipHash % MAX_DATACENTER_ID;
        } catch (Exception e) {
            log.warn("自动生成datacenterId失败,使用随机数", e);
            return (long) (Math.random() * MAX_DATACENTER_ID);
        }
    }
}

2.3 FileUtil(文件操作工具)

java 复制代码
package com.xxx.common.business.util;

import com.xxx.common.enums.ErrorCodeEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

/**
 * 文件操作工具类(兼容MinIO/本地存储,自动生成文件名)
 * 常用度:95%
 * 所属层级:业务层/控制层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Component
public class FileUtil {

    /**
     * 存储类型(local/minio)
     */
    @Value("${file.storage.type:local}")
    private String storageType;

    /**
     * 本地存储根路径
     */
    @Value("${file.storage.local.root-path:/data/files}")
    private String localRootPath;

    /**
     * MinIO相关配置(示例)
     */
    @Value("${file.storage.minio.endpoint:}")
    private String minioEndpoint;
    @Value("${file.storage.minio.bucket-name:}")
    private String minioBucketName;
    @Value("${file.storage.minio.access-key:}")
    private String minioAccessKey;
    @Value("${file.storage.minio.secret-key:}")
    private String minioSecretKey;

    /**
     * 文件上传
     */
    public String upload(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "上传文件不能为空");
        }

        // 校验文件大小(默认最大50MB)
        long maxSize = 50 * 1024 * 1024;
        if (file.getSize() > maxSize) {
            throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "文件大小不能超过50MB");
        }

        // 生成唯一文件名
        String originalFilename = file.getOriginalFilename();
        String ext = StringUtil.isBlank(originalFilename) ? "" :
                originalFilename.substring(originalFilename.lastIndexOf("."));
        String fileName = UUID.randomUUID().toString() + ext;

        // 按日期分目录
        String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));

        try {
            if ("local".equals(storageType)) {
                // 本地存储
                Path dirPath = Paths.get(localRootPath, dateDir);
                if (!Files.exists(dirPath)) {
                    Files.createDirectories(dirPath);
                }
                Path filePath = dirPath.resolve(fileName);
                file.transferTo(filePath);
                log.info("本地文件上传成功,路径:{}", filePath);
                return String.format("/%s/%s/%s", storageType, dateDir, fileName);
            } else if ("minio".equals(storageType)) {
                // MinIO存储(需引入minio依赖)
                /*
                MinioClient minioClient = MinioClient.builder()
                        .endpoint(minioEndpoint)
                        .credentials(minioAccessKey, minioSecretKey)
                        .build();
                // 检查桶是否存在
                if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(minioBucketName).build())) {
                    minioClient.makeBucket(MakeBucketArgs.builder().bucket(minioBucketName).build());
                }
                // 上传文件
                String objectName = String.format("%s/%s", dateDir, fileName);
                minioClient.putObject(
                        PutObjectArgs.builder()
                                .bucket(minioBucketName)
                                .object(objectName)
                                .stream(file.getInputStream(), file.getSize(), -1)
                                .contentType(file.getContentType())
                                .build()
                );
                log.info("MinIO文件上传成功,对象名:{}", objectName);
                return String.format("%s/%s/%s", minioEndpoint, minioBucketName, objectName);
                */
                throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "MinIO存储未实现");
            } else {
                throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "不支持的存储类型:" + storageType);
            }
        } catch (IOException e) {
            log.error("文件上传失败", e);
            throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "文件上传失败:" + e.getMessage());
        }
    }

    /**
     * 文件下载
     */
    public byte[] download(String filePath) {
        if (StringUtil.isBlank(filePath)) {
            throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "文件路径不能为空");
        }

        try {
            if (filePath.startsWith("/local/")) {
                // 本地文件下载
                String realPath = localRootPath + filePath.replace("/local/", "/");
                Path path = Paths.get(realPath);
                if (!Files.exists(path)) {
                    throw new BusinessException(ErrorCodeEnum.DATA_NOT_FOUND, "文件不存在");
                }
                return Files.readAllBytes(path);
            } else if (filePath.startsWith(minioEndpoint)) {
                // MinIO文件下载
                /*
                String objectName = filePath.replace(minioEndpoint + "/" + minioBucketName + "/", "");
                MinioClient minioClient = MinioClient.builder()
                        .endpoint(minioEndpoint)
                        .credentials(minioAccessKey, minioSecretKey)
                        .build();
                InputStream inputStream = minioClient.getObject(
                        GetObjectArgs.builder()
                                .bucket(minioBucketName)
                                .object(objectName)
                                .build()
                );
                return IOUtils.toByteArray(inputStream);
                */
                throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "MinIO下载未实现");
            } else {
                throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "不支持的文件路径:" + filePath);
            }
        } catch (IOException e) {
            log.error("文件下载失败", e);
            throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "文件下载失败:" + e.getMessage());
        }
    }

    /**
     * 文件删除
     */
    public void delete(String filePath) {
        if (StringUtil.isBlank(filePath)) {
            throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "文件路径不能为空");
        }

        try {
            if (filePath.startsWith("/local/")) {
                // 本地文件删除
                String realPath = localRootPath + filePath.replace("/local/", "/");
                Path path = Paths.get(realPath);
                if (Files.exists(path)) {
                    Files.delete(path);
                    log.info("本地文件删除成功,路径:{}", realPath);
                }
            } else if (filePath.startsWith(minioEndpoint)) {
                // MinIO文件删除
                /*
                String objectName = filePath.replace(minioEndpoint + "/" + minioBucketName + "/", "");
                MinioClient minioClient = MinioClient.builder()
                        .endpoint(minioEndpoint)
                        .credentials(minioAccessKey, minioSecretKey)
                        .build();
                minioClient.removeObject(
                        RemoveObjectArgs.builder()
                                .bucket(minioBucketName)
                                .object(objectName)
                                .build()
                );
                log.info("MinIO文件删除成功,对象名:{}", objectName);
                */
                throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "MinIO删除未实现");
            } else {
                throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "不支持的文件路径:" + filePath);
            }
        } catch (IOException e) {
            log.error("文件删除失败", e);
            throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "文件删除失败:" + e.getMessage());
        }
    }

    /**
     * 获取文件扩展名
     */
    public static String getFileExt(String fileName) {
        if (StringUtil.isBlank(fileName) || !fileName.contains(".")) {
            return "";
        }
        return fileName.substring(fileName.lastIndexOf("."));
    }

    /**
     * 校验文件类型(白名单)
     */
    public static boolean checkFileType(String fileName, String[] allowExts) {
        String ext = getFileExt(fileName).toLowerCase();
        for (String allowExt : allowExts) {
            if (ext.equals(allowExt.toLowerCase())) {
                return true;
            }
        }
        return false;
    }
}

2.4 MoneyUtil(金额计算工具)

java 复制代码
package com.xxx.common.business.util;

import com.xxx.common.util.NumberUtil;
import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * 金额计算工具类(BigDecimal封装,元转分/分转元)
 * 常用度:95%
 * 所属层级:业务层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
public class MoneyUtil {

    /**
     * 金额小数位数
     */
    private static final int MONEY_SCALE = 2;

    /**
     * 四舍五入模式
     */
    private static final RoundingMode ROUND_MODE = RoundingMode.HALF_UP;

    /**
     * 元转分(精确计算,避免浮点数误差)
     */
    public static long yuanToFen(BigDecimal yuan) {
        if (yuan == null) {
            return 0L;
        }
        // 先保留2位小数,再转分
        BigDecimal fen = yuan.setScale(MONEY_SCALE, ROUND_MODE).multiply(new BigDecimal("100"));
        return fen.longValue();
    }

    /**
     * 分转元(保留2位小数)
     */
    public static BigDecimal fenToYuan(long fen) {
        BigDecimal yuan = new BigDecimal(fen).divide(new BigDecimal("100"), MONEY_SCALE, ROUND_MODE);
        return yuan;
    }

    /**
     * 金额加法(元)
     */
    public static BigDecimal add(BigDecimal m1, BigDecimal m2) {
        return NumberUtil.add(m1, m2);
    }

    /**
     * 金额减法(元)
     */
    public static BigDecimal subtract(BigDecimal m1, BigDecimal m2) {
        return NumberUtil.subtract(m1, m2);
    }

    /**
     * 金额乘法(元,如:金额*折扣)
     */
    public static BigDecimal multiply(BigDecimal money, BigDecimal rate) {
        return money.multiply(rate).setScale(MONEY_SCALE, ROUND_MODE);
    }

    /**
     * 金额除法(元,如:金额/数量)
     */
    public static BigDecimal divide(BigDecimal money, int count) {
        if (count == 0) {
            log.warn("金额除法,除数为0");
            return BigDecimal.ZERO;
        }
        return money.divide(new BigDecimal(count), MONEY_SCALE, ROUND_MODE);
    }

    /**
     * 金额比较(m1 >= m2 返回true)
     */
    public static boolean ge(BigDecimal m1, BigDecimal m2) {
        return m1.compareTo(m2) >= 0;
    }

    /**
     * 金额比较(m1 > m2 返回true)
     */
    public static boolean gt(BigDecimal m1, BigDecimal m2) {
        return m1.compareTo(m2) > 0;
    }

    /**
     * 金额比较(m1 <= m2 返回true)
     */
    public static boolean le(BigDecimal m1, BigDecimal m2) {
        return m1.compareTo(m2) <= 0;
    }

    /**
     * 金额比较(m1 < m2 返回true)
     */
    public static boolean lt(BigDecimal m1, BigDecimal m2) {
        return m1.compareTo(m2) < 0;
    }

    /**
     * 金额格式化(保留2位小数,补零)
     */
    public static String format(BigDecimal money) {
        return money.setScale(MONEY_SCALE, ROUND_MODE).toString();
    }

    /**
     * 校验金额是否为正数
     */
    public static boolean isPositive(BigDecimal money) {
        return NumberUtil.isPositive(money);
    }

    /**
     * 校验金额是否为非负数
     */
    public static boolean isNonNegative(BigDecimal money) {
        return NumberUtil.isNonNegative(money);
    }
}

2.5 CacheUtil(本地缓存工具)

java 复制代码
package com.xxx.common.business.util;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.xxx.common.exception.SystemException;
import com.xxx.common.enums.ErrorCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * 本地缓存工具(基于Caffeine,支持过期时间、自动刷新)
 * 常用度:95%
 * 所属层级:业务层/DAO层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Component
public class CacheUtil {

    /**
     * 缓存容器(key:缓存名称,value:Caffeine缓存)
     */
    private final Map<String, Cache<Object, Object>> cacheContainer = new ConcurrentHashMap<>();

    /**
     * 默认缓存配置
     */
    private static final int DEFAULT_EXPIRE_TIME = 30; // 分钟
    private static final int DEFAULT_MAX_SIZE = 10000;

    /**
     * 初始化默认缓存
     */
    @PostConstruct
    public void init() {
        // 初始化通用缓存
        createCache("commonCache", DEFAULT_EXPIRE_TIME, DEFAULT_MAX_SIZE);
        log.info("本地缓存工具初始化完成");
    }

    /**
     * 创建缓存
     *
     * @param cacheName   缓存名称
     * @param expireTime  过期时间(分钟)
     * @param maxSize     最大容量
     */
    public void createCache(String cacheName, int expireTime, int maxSize) {
        Cache<Object, Object> cache = Caffeine.newBuilder()
                .maximumSize(maxSize)
                .expireAfterWrite(expireTime, TimeUnit.MINUTES)
                .recordStats() // 记录缓存统计信息
                .removalListener((key, value, cause) ->
                        log.debug("缓存[key={}]被移除,原因:{}", key, cause.name()))
                .build();
        cacheContainer.put(cacheName, cache);
        log.info("创建缓存成功,名称:{},过期时间:{}分钟,最大容量:{}", cacheName, expireTime, maxSize);
    }

    /**
     * 创建带自动刷新的缓存
     *
     * @param cacheName    缓存名称
     * @param expireTime   过期时间(分钟)
     * @param refreshTime  刷新时间(分钟)
     * @param maxSize      最大容量
     */
    public void createRefreshCache(String cacheName, int expireTime, int refreshTime, int maxSize) {
        Cache<Object, Object> cache = Caffeine.newBuilder()
                .maximumSize(maxSize)
                .expireAfterWrite(expireTime, TimeUnit.MINUTES)
                .refreshAfterWrite(refreshTime, TimeUnit.MINUTES)
                .recordStats()
                .removalListener((key, value, cause) ->
                        log.debug("缓存[key={}]被移除,原因:{}", key, cause.name()))
                .build();
        cacheContainer.put(cacheName, cache);
        log.info("创建自动刷新缓存成功,名称:{},过期时间:{}分钟,刷新时间:{}分钟,最大容量:{}",
                cacheName, expireTime, refreshTime, maxSize);
    }

    /**
     * 获取缓存(不存在则创建)
     */
    private Cache<Object, Object> getCache(String cacheName) {
        return cacheContainer.computeIfAbsent(cacheName, k -> {
            log.warn("缓存[{}]不存在,创建默认配置缓存", k);
            return Caffeine.newBuilder()
                    .maximumSize(DEFAULT_MAX_SIZE)
                    .expireAfterWrite(DEFAULT_EXPIRE_TIME, TimeUnit.MINUTES)
                    .build();
        });
    }

    /**
     * 添加缓存
     */
    public void put(String cacheName, Object key, Object value) {
        if (key == null) {
            log.warn("缓存key不能为空");
            return;
        }
        getCache(cacheName).put(key, value);
    }

    /**
     * 获取缓存(无则返回null)
     */
    @SuppressWarnings("unchecked")
    public <T> T get(String cacheName, Object key) {
        if (key == null) {
            return null;
        }
        return (T) getCache(cacheName).getIfPresent(key);
    }

    /**
     * 获取缓存(无则加载)
     */
    @SuppressWarnings("unchecked")
    public <T> T get(String cacheName, Object key, Function<Object, T> loader) {
        if (key == null) {
            throw new SystemException(ErrorCodeEnum.PARAM_INVALID, "缓存key不能为空");
        }
        try {
            return (T) getCache(cacheName).get(key, k -> loader.apply(k));
        } catch (Exception e) {
            log.error("缓存加载失败,cacheName={}, key={}", cacheName, key, e);
            throw new SystemException(ErrorCodeEnum.SYSTEM_ERROR, "缓存加载失败");
        }
    }

    /**
     * 删除缓存
     */
    public void remove(String cacheName, Object key) {
        if (key == null) {
            return;
        }
        getCache(cacheName).invalidate(key);
    }

    /**
     * 清空缓存
     */
    public void clear(String cacheName) {
        getCache(cacheName).invalidateAll();
        log.info("清空缓存成功,名称:{}", cacheName);
    }

    /**
     * 获取缓存统计信息
     */
    public String getStats(String cacheName) {
        return getCache(cacheName).stats().toString();
    }

    /**
     * 获取通用缓存(默认缓存)
     */
    public <T> T getCommonCache(Object key) {
        return get("commonCache", key);
    }

    /**
     * 添加通用缓存
     */
    public void putCommonCache(Object key, Object value) {
        put("commonCache", key, value);
    }
}

四、安全防护模块(上线前必用,95%通用)

1. 安全工具类

1.1 XssFilter(防XSS攻击过滤器)

java 复制代码
package com.xxx.common.security.filter;

import com.xxx.common.util.StringUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.List;
import java.util.regex.Pattern;

/**
 * XSS攻击防护过滤器(过滤请求参数中的脚本,支持白名单)
 * 常用度:95%
 * 所属层级:控制层(过滤器)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Component
@WebFilter(urlPatterns = "/*")
@Order(1) // 优先级最高
public class XssFilter implements Filter {

    /**
     * XSS白名单接口(逗号分隔)
     */
    @Value("${security.xss.whitelist:/api/v1/public/*,/swagger-ui/**}")
    private String xssWhitelist;

    private List<String> whitelistPatterns;

    /**
     * XSS过滤正则
     */
    private static final Pattern XSS_PATTERN = Pattern.compile(
            "<script.*?>.*?</script.*?>|javascript:|onload=|onclick=|onmouseover=|onerror=|onfocus=|eval\\(|alert\\(|confirm\\(|prompt\\(|iframe|frame|script",
            Pattern.CASE_INSENSITIVE
    );

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化白名单
        whitelistPatterns = List.of(xssWhitelist.split(","));
        log.info("XSS过滤器初始化完成,白名单:{}", whitelistPatterns);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 检查是否在白名单中
        String requestURI = httpRequest.getRequestURI();
        if (isWhitelist(requestURI)) {
            chain.doFilter(request, response);
            return;
        }

        // 包装请求,过滤参数
        XssHttpServletRequestWrapper wrappedRequest = new XssHttpServletRequestWrapper(httpRequest);
        chain.doFilter(wrappedRequest, httpResponse);
    }

    /**
     * 判断是否在白名单中
     */
    private boolean isWhitelist(String requestURI) {
        for (String pattern : whitelistPatterns) {
            if (StringUtil.isBlank(pattern)) {
                continue;
            }
            // 支持通配符*
            String regex = pattern.replace("*", ".*");
            if (requestURI.matches(regex)) {
                return true;
            }
        }
        return false;
    }

    /**
     * XSS请求包装器
     */
    private static class XssHttpServletRequestWrapper extends jakarta.servlet.http.HttpServletRequestWrapper {

        public XssHttpServletRequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public String getParameter(String name) {
            String value = super.getParameter(name);
            return filterXss(value);
        }

        @Override
        public String[] getParameterValues(String name) {
            String[] values = super.getParameterValues(name);
            if (values == null) {
                return null;
            }
            for (int i = 0; i < values.length; i++) {
                values[i] = filterXss(values[i]);
            }
            return values;
        }

        @Override
        public String getHeader(String name) {
            String value = super.getHeader(name);
            return filterXss(value);
        }

        /**
         * XSS过滤
         */
        private String filterXss(String value) {
            if (StringUtil.isBlank(value)) {
                return value;
            }
            // 替换危险字符
            value = XSS_PATTERN.matcher(value).replaceAll("");
            // 转义HTML特殊字符
            value = value.replace("<", "&lt;")
                    .replace(">", "&gt;")
                    .replace("&", "&amp;")
                    .replace("\"", "&quot;")
                    .replace("'", "&#39;");
            return value;
        }
    }

    @Override
    public void destroy() {
        log.info("XSS过滤器销毁");
    }
}

1.2 SensitiveUtil(敏感信息脱敏工具)

java 复制代码
package com.xxx.common.security.util;

import com.xxx.common.util.StringUtil;

/**
 * 敏感信息脱敏工具(统一脱敏规则)
 * 常用度:95%
 * 所属层级:通用层(控制/业务)
 *
 * @author xxx
 * @date 2025-11-30
 */
public class SensitiveUtil {

    /**
     * 手机号脱敏(138****1234)
     */
    public static String maskMobile(String mobile) {
        return StringUtil.maskMobile(mobile);
    }

    /**
     * 身份证脱敏(110101********1234)
     */
    public static String maskIdCard(String idCard) {
        return StringUtil.maskIdCard(idCard);
    }

    /**
     * 银行卡脱敏(6226****1234)
     */
    public static String maskBankCard(String bankCard) {
        return StringUtil.maskBankCard(bankCard);
    }

    /**
     * 姓名脱敏(张*三 / 李*)
     */
    public static String maskName(String name) {
        return StringUtil.maskName(name);
    }

    /**
     * 邮箱脱敏(z***@xxx.com)
     */
    public static String maskEmail(String email) {
        if (StringUtil.isBlank(email) || !email.contains("@")) {
            return email;
        }
        String[] parts = email.split("@");
        String prefix = parts[0];
        String suffix = parts[1];
        if (prefix.length() <= 1) {
            return "*@" + suffix;
        } else if (prefix.length() == 2) {
            return prefix.substring(0, 1) + "*@" + suffix;
        } else {
            return prefix.substring(0, 1) + "***" + prefix.substring(prefix.length() - 1) + "@" + suffix;
        }
    }

    /**
     * 密码脱敏(全部替换为*)
     */
    public static String maskPassword(String password) {
        if (StringUtil.isBlank(password)) {
            return "";
        }
        return "******";
    }

    /**
     * 地址脱敏(北京市朝阳区******)
     */
    public static String maskAddress(String address) {
        if (StringUtil.isBlank(address)) {
            return address;
        }
        int length = address.length();
        if (length <= 6) {
            return address.substring(0, 3) + "***";
        } else {
            return address.substring(0, 6) + "******";
        }
    }

    /**
     * 自定义脱敏(保留前prefixLen位,后suffixLen位,中间用*填充)
     */
    public static String maskCustom(String content, int prefixLen, int suffixLen) {
        if (StringUtil.isBlank(content)) {
            return content;
        }
        int length = content.length();
        if (length <= prefixLen + suffixLen) {
            return content.substring(0, prefixLen) + "*".repeat(length - prefixLen);
        }
        String prefix = content.substring(0, prefixLen);
        String suffix = content.substring(length - suffixLen);
        return prefix + "*".repeat(length - prefixLen - suffixLen) + suffix;
    }
}

1.3 TokenUtil(JWT工具类)

java 复制代码
package com.xxx.common.security.util;

import com.xxx.common.enums.ErrorCodeEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.util.DateUtil;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

/**
 * JWT Token工具类(生成/解析/刷新)
 * 常用度:95%
 * 所属层级:控制层/业务层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Component
public class TokenUtil {

    /**
     * JWT密钥(生产环境从配置中心读取,长度至少32位)
     */
    @Value("${security.jwt.secret:xxx1234567890abcdefxxx1234567890abc}")
    private String jwtSecret;

    /**
     * Token过期时间(分钟)
     */
    @Value("${security.jwt.expire-time:120}")
    private int expireTime;

    /**
     * 刷新Token过期时间(天)
     */
    @Value("${security.jwt.refresh-expire-time:7}")
    private int refreshExpireTime;

    /**
     * 生成访问Token
     */
    public String generateToken(String userId, Map<String, Object> claims) {
        // 生成密钥
        SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));

        // 过期时间
        Date expireDate = new Date(System.currentTimeMillis() + expireTime * 60 * 1000L);

        // 构建Token
        return Jwts.builder()
                .setClaims(claims) // 自定义载荷
                .setSubject(userId) // 主题(用户ID)
                .setIssuedAt(new Date()) // 签发时间
                .setExpiration(expireDate) // 过期时间
                .signWith(key, SignatureAlgorithm.HS256) // 签名算法
                .compact();
    }

    /**
     * 生成刷新Token
     */
    public String generateRefreshToken(String userId) {
        SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
        Date expireDate = new Date(System.currentTimeMillis() + refreshExpireTime * 24 * 60 * 1000L);
        return Jwts.builder()
                .setSubject(userId)
                .setIssuedAt(new Date())
                .setExpiration(expireDate)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    /**
     * 解析Token
     */
    public Claims parseToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            log.error("Token已过期", e);
            throw new BusinessException(ErrorCodeEnum.TOKEN_INVALID, "Token已过期");
        } catch (UnsupportedJwtException e) {
            log.error("不支持的Token格式", e);
            throw new BusinessException(ErrorCodeEnum.TOKEN_INVALID, "不支持的Token格式");
        } catch (MalformedJwtException e) {
            log.error("Token格式错误", e);
            throw new BusinessException(ErrorCodeEnum.TOKEN_INVALID, "Token格式错误");
        } catch (SignatureException e) {
            log.error("Token签名验证失败", e);
            throw new BusinessException(ErrorCodeEnum.TOKEN_INVALID, "Token签名验证失败");
        } catch (IllegalArgumentException e) {
            log.error("Token为空或无效", e);
            throw new BusinessException(ErrorCodeEnum.TOKEN_MISSING, "Token不能为空");
        } catch (Exception e) {
            log.error("Token解析失败", e);
            throw new BusinessException(ErrorCodeEnum.TOKEN_INVALID, "Token解析失败");
        }
    }

    /**
     * 验证Token是否有效
     */
    public boolean validateToken(String token) {
        try {
            parseToken(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 从Token中获取用户ID
     */
    public String getUserIdFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.getSubject();
    }

    /**
     * 从Token中获取自定义字段
     */
    public <T> T getClaimFromToken(String token, String claimName, Class<T> clazz) {
        Claims claims = parseToken(token);
        return claims.get(claimName, clazz);
    }

    /**
     * 刷新Token(原Token未过期时刷新)
     */
    public String refreshToken(String oldToken) {
        Claims claims = parseToken(oldToken);
        // 检查原Token剩余有效期(至少剩余5分钟才允许刷新)
        Date expireDate = claims.getExpiration();
        long remainTime = expireDate.getTime() - System.currentTimeMillis();
        if (remainTime < 5 * 60 * 1000L) {
            throw new BusinessException(ErrorCodeEnum.TOKEN_INVALID, "Token即将过期,无法刷新");
        }
        // 生成新Token
        return generateToken(claims.getSubject(), claims);
    }

    /**
     * 获取Token过期时间
     */
    public String getTokenExpireTime(String token) {
        Claims claims = parseToken(token);
        return DateUtil.format(DateUtil.dateToLocalDateTime(claims.getExpiration()));
    }
}

1.4 SignUtil(接口签名工具)

java 复制代码
package com.xxx.common.security.util;

import com.xxx.common.enums.ErrorCodeEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

/**
 * 接口签名工具(防止参数篡改,支持超时校验)
 * 常用度:95%
 * 所属层级:控制层/业务层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Component
public class SignUtil {

    /**
     * 签名密钥(生产环境从配置中心读取)
     */
    @Value("${security.sign.secret:xxx_sign_key_2025}")
    private String signSecret;

    /**
     * 签名超时时间(秒)
     */
    @Value("${security.sign.timeout:5}")
    private int signTimeout;

    /**
     * 生成签名
     *
     * @param params   请求参数(不含sign)
     * @param timestamp 时间戳(毫秒)
     * @return 签名
     */
    public String generateSign(Map<String, Object> params, long timestamp) {
        // 1. 参数排序(按key升序)
        List<String> sortedKeys = new ArrayList<>(params.keySet());
        Collections.sort(sortedKeys);

        // 2. 拼接参数字符串(key=value&key=value)
        StringBuilder sb = new StringBuilder();
        for (String key : sortedKeys) {
            Object value = params.get(key);
            if (value == null || StringUtil.isBlank(value.toString())) {
                continue;
            }
            sb.append(key).append("=").append(value).append("&");
        }

        // 3. 拼接时间戳和密钥
        sb.append("timestamp=").append(timestamp).append("&secret=").append(signSecret);

        // 4. MD5加密
        return md5(sb.toString());
    }

    /**
     * 验证签名
     *
     * @param params   请求参数(含sign)
     * @param sign     客户端传入的签名
     * @return 是否有效
     */
    public boolean verifySign(Map<String, Object> params, String sign) {
        // 1. 复制参数,移除sign字段
        Map<String, Object> copyParams = new HashMap<>(params);
        copyParams.remove("sign");

        // 2. 获取时间戳
        Object timestampObj = copyParams.get("timestamp");
        if (timestampObj == null) {
            log.warn("签名验证失败:缺少timestamp参数");
            return false;
        }
        long timestamp;
        try {
            timestamp = Long.parseLong(timestampObj.toString());
        } catch (NumberFormatException e) {
            log.warn("签名验证失败:timestamp格式错误");
            return false;
        }

        // 3. 校验超时
        long currentTime = System.currentTimeMillis();
        long diff = Math.abs(currentTime - timestamp);
        if (diff > signTimeout * 1000L) {
            log.warn("签名验证失败:请求超时,timestamp={}, currentTime={}, diff={}ms",
                    timestamp, currentTime, diff);
            return false;
        }

        // 4. 生成服务端签名
        String serverSign = generateSign(copyParams, timestamp);

        // 5. 对比签名
        boolean valid = serverSign.equals(sign);
        if (!valid) {
            log.warn("签名验证失败:客户端签名={}, 服务端签名={}", sign, serverSign);
        }
        return valid;
    }

    /**
     * 验证签名(抛出异常)
     */
    public void validateSign(Map<String, Object> params, String sign) {
        if (!verifySign(params, sign)) {
            throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "签名验证失败");
        }
    }

    /**
     * MD5加密
     */
    private String md5(String content) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(content.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : bytes) {
                String hex = Integer.toHexString(b & 0xFF);
                if (hex.length() == 1) {
                    sb.append("0");
                }
                sb.append(hex);
            }
            return sb.toString().toUpperCase();
        } catch (NoSuchAlgorithmException e) {
            log.error("MD5加密失败", e);
            throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "签名生成失败");
        }
    }
}

2. 防护组件

2.1 IdempotentAOP(幂等性注解AOP)

java 复制代码
package com.xxx.common.security.aop;

import com.xxx.common.enums.ErrorCodeEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * 幂等性注解AOP(基于Redis,支持SpEL表达式)
 * 常用度:95%
 * 所属层级:业务层(AOP)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Aspect
@Component
public class IdempotentAOP {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 幂等性注解
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Idempotent {
        /**
         * 幂等键(SpEL表达式,如:#orderNo、#user.id)
         */
        String key();

        /**
         * 过期时间(秒)
         */
        int expireTime() default 60;

        /**
         * 重复提交提示语
         */
        String message() default "请勿重复提交";
    }

    /**
     * 切点
     */
    @Pointcut("@annotation(com.xxx.common.security.aop.IdempotentAOP.Idempotent)")
    public void idempotentPointcut() {
    }

    /**
     * 环绕通知
     */
    @Around("idempotentPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取注解信息
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Idempotent idempotent = signature.getMethod().getAnnotation(Idempotent.class);
        String keyExpression = idempotent.key();
        int expireTime = idempotent.expireTime();
        String message = idempotent.message();

        // 解析SpEL表达式,生成幂等键
        String idempotentKey = parseSpEL(keyExpression, joinPoint);
        if (StringUtil.isBlank(idempotentKey)) {
            throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "幂等键不能为空");
        }
        String redisKey = "idempotent:" + idempotentKey;

        // 尝试设置Redis锁
        Boolean success = redisTemplate.opsForValue().setIfAbsent(redisKey, "1", expireTime, TimeUnit.SECONDS);
        if (Boolean.FALSE.equals(success)) {
            log.warn("幂等性校验失败,重复请求,key:{}", redisKey);
            throw new BusinessException(ErrorCodeEnum.BUSINESS_COMMON_ERROR, message);
        }

        try {
            // 执行目标方法
            return joinPoint.proceed();
        } catch (Exception e) {
            // 业务异常,释放锁
            redisTemplate.delete(redisKey);
            throw e;
        }
    }

    /**
     * 解析SpEL表达式
     */
    private String parseSpEL(String expression, ProceedingJoinPoint joinPoint) {
        // 获取方法参数
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String[] paramNames = signature.getParameterNames();
        Object[] args = joinPoint.getArgs();

        // 构建SpEL上下文
        EvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < paramNames.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }

        // 解析表达式
        ExpressionParser parser = new SpelExpressionParser();
        Object value = parser.parseExpression(expression).getValue(context);
        return value == null ? "" : value.toString();
    }
}

2.2 AntiBrushInterceptor(接口防刷拦截器)

java 复制代码
package com.xxx.common.security.interceptor;

import com.xxx.common.enums.ErrorCodeEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.util.StringUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 接口防刷拦截器(按IP+接口限流,支持黑名单、阈值配置)
 * 常用度:95%
 * 所属层级:控制层(拦截器)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Component
public class AntiBrushInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 防刷白名单接口(逗号分隔,支持通配符*)
     */
    @Value("${security.anti-brush.whitelist:/api/v1/public/*,/swagger-ui/**,/actuator/**}")
    private String antiBrushWhitelist;

    /**
     * 默认限流阈值(单位时间内最大请求数)
     */
    @Value("${security.anti-brush.default-limit:60}")
    private int defaultLimit;

    /**
     * 限流时间窗口(秒)
     */
    @Value("${security.anti-brush.time-window:60}")
    private int timeWindow;

    /**
     * 黑名单前缀
     */
    private static final String BLACKLIST_KEY_PREFIX = "anti_brush:blacklist:";

    /**
     * 限流计数前缀
     */
    private static final String LIMIT_KEY_PREFIX = "anti_brush:limit:";

    /**
     * 黑名单过期时间(小时)
     */
    private static final int BLACKLIST_EXPIRE_HOURS = 24;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        String ip = getIpAddress(request);

        // 1. 检查IP是否在黑名单
        if (isBlacklisted(ip)) {
            log.warn("IP[{}]访问接口[{}]被拦截:已加入黑名单", ip, requestURI);
            throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "请求过于频繁,请24小时后重试");
        }

        // 2. 检查是否在白名单
        if (isWhitelist(requestURI)) {
            return true;
        }

        // 3. 接口限流校验
        String limitKey = LIMIT_KEY_PREFIX + ip + ":" + requestURI;
        Long count = redisTemplate.opsForValue().increment(limitKey, 1);
        // 首次访问,设置过期时间
        if (count != null && count == 1) {
            redisTemplate.expire(limitKey, timeWindow, TimeUnit.SECONDS);
        }

        // 4. 超过阈值,加入黑名单
        if (count != null && count > defaultLimit) {
            addToBlacklist(ip);
            log.warn("IP[{}]访问接口[{}]触发限流:请求次数={},阈值={}", ip, requestURI, count, defaultLimit);
            throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "请求过于频繁,请稍后重试");
        }

        return true;
    }

    /**
     * 获取客户端真实IP
     */
    private String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 处理多IP场景(X-Forwarded-For可能包含多个IP,取第一个)
        if (StringUtil.isNotBlank(ip) && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return StringUtil.isBlank(ip) ? "127.0.0.1" : ip;
    }

    /**
     * 判断是否在白名单
     */
    private boolean isWhitelist(String requestURI) {
        List<String> whitelist = List.of(antiBrushWhitelist.split(","));
        for (String pattern : whitelist) {
            if (StringUtil.isBlank(pattern)) {
                continue;
            }
            String regex = pattern.replace("*", ".*");
            if (requestURI.matches(regex)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 判断IP是否在黑名单
     */
    private boolean isBlacklisted(String ip) {
        String blacklistKey = BLACKLIST_KEY_PREFIX + ip;
        return Boolean.TRUE.equals(redisTemplate.hasKey(blacklistKey));
    }

    /**
     * 将IP加入黑名单
     */
    private void addToBlacklist(String ip) {
        String blacklistKey = BLACKLIST_KEY_PREFIX + ip;
        redisTemplate.opsForValue().set(blacklistKey, "1", BLACKLIST_EXPIRE_HOURS, TimeUnit.HOURS);
        log.info("IP[{}]被加入防刷黑名单,过期时间:{}小时", ip, BLACKLIST_EXPIRE_HOURS);
    }

    /**
     * 移除黑名单IP(手动解除)
     */
    public void removeBlacklist(String ip) {
        String blacklistKey = BLACKLIST_KEY_PREFIX + ip;
        redisTemplate.delete(blacklistKey);
        log.info("IP[{}]被移出防刷黑名单", ip);
    }

    /**
     * 自定义接口限流阈值(覆盖默认值)
     */
    public void setInterfaceLimit(String requestURI, int limit) {
        String limitConfigKey = "anti_brush:config:" + requestURI;
        redisTemplate.opsForValue().set(limitConfigKey, limit, 7, TimeUnit.DAYS);
        log.info("接口[{}]限流阈值设置为:{}", requestURI, limit);
    }
}

// 拦截器配置类
package com.xxx.common.config;

import com.xxx.common.security.interceptor.AntiBrushInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器注册配置
 * 常用度:95%
 * 所属层级:配置层
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private AntiBrushInterceptor antiBrushInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册接口防刷拦截器
        registry.addInterceptor(antiBrushInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/swagger-ui/**", "/v3/api-docs/**", "/actuator/**");
    }
}

2.3 SqlInjectFilter(防SQL注入过滤器)

java 复制代码
package com.xxx.common.security.filter;

import com.xxx.common.util.StringUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.List;
import java.util.regex.Pattern;

/**
 * 防SQL注入过滤器(过滤危险关键字,支持白名单)
 * 常用度:95%
 * 所属层级:控制层(过滤器)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Component
@WebFilter(urlPatterns = "/*")
@Order(2) // 优先级低于XSS过滤器
public class SqlInjectFilter implements Filter {

    /**
     * SQL注入白名单接口
     */
    @Value("${security.sql-inject.whitelist:/api/v1/public/*,/swagger-ui/**,/actuator/**}")
    private String sqlInjectWhitelist;

    /**
     * SQL注入危险关键字正则(忽略大小写)
     */
    private static final Pattern SQL_INJECT_PATTERN = Pattern.compile(
            "(\\b(ALTER|CREATE|DELETE|DROP|EXEC|INSERT|MERGE|SELECT|UPDATE|UNION|AND|OR|NOT|WHERE)\\b)|(--)|(;)|(\\|\\|)|(\\bEXECUTE\\b)|(\\bDECLARE\\b)",
            Pattern.CASE_INSENSITIVE
    );

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();

        // 检查白名单
        if (isWhitelist(requestURI)) {
            chain.doFilter(request, response);
            return;
        }

        // 包装请求,过滤SQL注入关键字
        SqlInjectRequestWrapper wrappedRequest = new SqlInjectRequestWrapper(httpRequest);
        chain.doFilter(wrappedRequest, response);
    }

    /**
     * 判断是否在白名单
     */
    private boolean isWhitelist(String requestURI) {
        List<String> whitelist = List.of(sqlInjectWhitelist.split(","));
        for (String pattern : whitelist) {
            if (StringUtil.isBlank(pattern)) {
                continue;
            }
            String regex = pattern.replace("*", ".*");
            if (requestURI.matches(regex)) {
                return true;
            }
        }
        return false;
    }

    /**
     * SQL注入请求包装器
     */
    private static class SqlInjectRequestWrapper extends jakarta.servlet.http.HttpServletRequestWrapper {

        public SqlInjectRequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public String getParameter(String name) {
            String value = super.getParameter(name);
            return filterSqlInject(value);
        }

        @Override
        public String[] getParameterValues(String name) {
            String[] values = super.getParameterValues(name);
            if (values == null) {
                return null;
            }
            for (int i = 0; i < values.length; i++) {
                values[i] = filterSqlInject(values[i]);
            }
            return values;
        }

        @Override
        public String getHeader(String name) {
            String value = super.getHeader(name);
            return filterSqlInject(value);
        }

        /**
         * 过滤SQL注入关键字
         */
        private String filterSqlInject(String value) {
            if (StringUtil.isBlank(value)) {
                return value;
            }
            // 替换危险关键字为空格
            value = SQL_INJECT_PATTERN.matcher(value).replaceAll(" ");
            log.debug("SQL注入过滤后的值:{}", value);
            return value;
        }
    }
}

五、监控审计模块(上线前必用,90%通用)

1. 日志审计

1.1 OperateLogAOP(操作日志AOP)

java 复制代码
package com.xxx.common.monitor.aop;

import com.xxx.common.util.TraceIdUtil;
import com.xxx.common.util.StringUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.annotation.*;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

/**
 * 操作日志AOP(注解式记录,支持自定义描述)
 * 常用度:90%
 * 所属层级:通用层(AOP)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Aspect
@Component
public class OperateLogAOP {

    @Autowired(required = false)
    private HttpServletRequest request;

    /**
     * 操作日志注解
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface OperateLog {
        /**
         * 操作描述
         */
        String value() default "";

        /**
         * 操作类型(如:ADD/UPDATE/DELETE/QUERY)
         */
        String type() default "UNKNOWN";

        /**
         * 是否记录请求参数
         */
        boolean recordParams() default true;

        /**
         * 是否记录返回结果
         */
        boolean recordResult() default false;
    }

    /**
     * 切点:所有标注@OperateLog的方法
     */
    @Pointcut("@annotation(com.xxx.common.monitor.aop.OperateLogAOP.OperateLog)")
    public void operateLogPointcut() {
    }

    /**
     * 环绕通知:记录操作日志
     */
    @Around("operateLogPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取注解信息
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        OperateLog operateLog = signature.getMethod().getAnnotation(OperateLog.class);
        String desc = operateLog.value();
        String type = operateLog.type();
        boolean recordParams = operateLog.recordParams();
        boolean recordResult = operateLog.recordResult();

        // 2. 构建日志上下文
        Map<String, Object> logContext = new HashMap<>();
        logContext.put("traceId", TraceIdUtil.getTraceId());
        logContext.put("operateTime", LocalDateTime.now());
        logContext.put("operateDesc", desc);
        logContext.put("operateType", type);
        logContext.put("className", joinPoint.getTarget().getClass().getName());
        logContext.put("methodName", signature.getMethod().getName());

        // 3. 获取请求信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            logContext.put("requestURI", request.getRequestURI());
            logContext.put("requestMethod", request.getMethod());
            logContext.put("clientIp", getClientIp(request));
            logContext.put("userAgent", request.getHeader("User-Agent"));
        }

        // 4. 记录请求参数
        if (recordParams) {
            String[] paramNames = signature.getParameterNames();
            Object[] args = joinPoint.getArgs();
            Map<String, Object> params = new HashMap<>();
            for (int i = 0; i < paramNames.length; i++) {
                // 敏感参数脱敏(如:password、mobile)
                Object value = args[i];
                if (paramNames[i].toLowerCase().contains("password")) {
                    value = "******";
                } else if (paramNames[i].toLowerCase().contains("mobile")) {
                    value = StringUtil.maskMobile(value.toString());
                }
                params.put(paramNames[i], value);
            }
            logContext.put("requestParams", params);
        }

        long startTime = System.currentTimeMillis();
        Object result = null;
        boolean success = true;
        String errorMsg = "";

        try {
            // 5. 执行目标方法
            result = joinPoint.proceed();
            return result;
        } catch (Exception e) {
            success = false;
            errorMsg = e.getMessage();
            throw e;
        } finally {
            // 6. 记录日志
            long costTime = System.currentTimeMillis() - startTime;
            logContext.put("costTime", costTime + "ms");
            logContext.put("success", success);
            logContext.put("errorMsg", errorMsg);
            if (recordResult && success) {
                logContext.put("responseResult", result);
            }

            // 7. 输出结构化日志(JSON格式)
            log.info("【操作日志】{}", logContext);

            // 扩展:可将日志写入数据库/ES等存储介质
            // saveOperateLog(logContext);
        }
    }

    /**
     * 获取客户端IP
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return StringUtil.isBlank(ip) ? "127.0.0.1" : ip.split(",")[0].trim();
    }

    /**
     * 保存操作日志到存储介质(示例,需根据业务实现)
     */
    private void saveOperateLog(Map<String, Object> logContext) {
        // 此处可实现日志入库/入ES等逻辑
        // log.info("保存操作日志到数据库:{}", logContext);
    }
}

1.2 TraceIdUtil(全链路TraceId工具)

java 复制代码
package com.xxx.common.util;

import org.slf4j.MDC;

import java.util.UUID;

/**
 * 全链路TraceId工具(生成/传递TraceId,放入MDC)
 * 常用度:90%
 * 所属层级:通用层
 *
 * @author xxx
 * @date 2025-11-30
 */
public class TraceIdUtil {

    /**
     * TraceId MDC键名
     */
    private static final String TRACE_ID_KEY = "traceId";

    /**
     * 生成TraceId(UUID去除横线)
     */
    public static String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    /**
     * 获取当前TraceId(MDC中)
     */
    public static String getTraceId() {
        String traceId = MDC.get(TRACE_ID_KEY);
        if (StringUtil.isBlank(traceId)) {
            traceId = generateTraceId();
            MDC.put(TRACE_ID_KEY, traceId);
        }
        return traceId;
    }

    /**
     * 设置TraceId到MDC
     */
    public static void setTraceId(String traceId) {
        if (StringUtil.isNotBlank(traceId)) {
            MDC.put(TRACE_ID_KEY, traceId);
        }
    }

    /**
     * 清除MDC中的TraceId
     */
    public static void clearTraceId() {
        MDC.remove(TRACE_ID_KEY);
    }

    /**
     * 从请求头获取TraceId(跨服务传递)
     */
    public static String getTraceIdFromHeader(jakarta.servlet.http.HttpServletRequest request) {
        String traceId = request.getHeader("X-Trace-Id");
        if (StringUtil.isBlank(traceId)) {
            traceId = request.getParameter("traceId");
        }
        return StringUtil.isNotBlank(traceId) ? traceId : generateTraceId();
    }
}

// TraceId过滤器(请求入口生成/传递TraceId)
package com.xxx.common.monitor.filter;

import com.xxx.common.util.TraceIdUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * TraceId过滤器(请求入口初始化TraceId)
 * 常用度:90%
 * 所属层级:控制层(过滤器)
 */
@Slf4j
@Component
@WebFilter(urlPatterns = "/*")
@Order(0) // 优先级最高
public class TraceIdFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        try {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            // 从请求头获取TraceId,无则生成
            String traceId = TraceIdUtil.getTraceIdFromHeader(httpRequest);
            TraceIdUtil.setTraceId(traceId);
            // 响应头返回TraceId
            ((jakarta.servlet.http.HttpServletResponse) response).setHeader("X-Trace-Id", traceId);
            chain.doFilter(request, response);
        } finally {
            // 清除MDC,避免线程复用导致污染
            TraceIdUtil.clearTraceId();
        }
    }
}

1.3 LogFormatter(日志格式化工具)

java 复制代码
package com.xxx.common.monitor.util;

import com.alibaba.fastjson2.JSON;
import com.xxx.common.enums.EnvEnum;
import com.xxx.common.util.StringUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

/**
 * 日志格式化工具(统一输出格式,支持JSON/文本切换)
 * 常用度:90%
 * 所属层级:通用层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Component
public class LogFormatter {

    /**
     * 日志输出格式(json/text)
     */
    @Value("${logging.format.type:json}")
    private String logFormatType;

    /**
     * 环境标识
     */
    @Value("${spring.profiles.active:dev}")
    private String env;

    /**
     * 应用名称
     */
    @Value("${spring.application.name:unknown-app}")
    private String appName;

    /**
     * 通用日期格式
     */
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");

    /**
     * 格式化日志(通用方法)
     */
    public String format(String level, String traceId, String module, String message, Object... args) {
        // 格式化消息参数
        String formattedMsg = StringUtil.isBlank(message) ? "" : String.format(message, args);

        // 构建日志上下文
        Map<String, Object> logMap = new HashMap<>();
        logMap.put("timestamp", LocalDateTime.now().format(DATE_FORMATTER));
        logMap.put("level", level);
        logMap.put("traceId", traceId);
        logMap.put("module", module);
        logMap.put("appName", appName);
        logMap.put("env", env);
        logMap.put("message", formattedMsg);

        // 生产环境隐藏敏感信息
        if (EnvEnum.getByCode(env).isProd()) {
            // 脱敏处理
            logMap.put("message", maskSensitiveInfo(formattedMsg));
        }

        // 按格式输出
        if ("json".equalsIgnoreCase(logFormatType)) {
            return JSON.toJSONString(logMap);
        } else {
            return String.format("[%s] [%s] [%s] [%s] [%s] %s",
                    logMap.get("timestamp"),
                    logMap.get("level"),
                    logMap.get("appName"),
                    logMap.get("traceId"),
                    logMap.get("module"),
                    logMap.get("message"));
        }
    }

    /**
     * 格式化业务日志
     */
    public String formatBusinessLog(String traceId, String module, String message, Object... args) {
        return format("INFO", traceId, module, message, args);
    }

    /**
     * 格式化异常日志
     */
    public String formatExceptionLog(String traceId, String module, String message, Throwable e) {
        Map<String, Object> exceptionMap = new HashMap<>();
        exceptionMap.put("exceptionType", e.getClass().getName());
        exceptionMap.put("exceptionMsg", e.getMessage());
        // 生产环境不打印堆栈
        if (!EnvEnum.getByCode(env).isProd()) {
            exceptionMap.put("stackTrace", getStackTrace(e));
        }

        String exceptionMsg = JSON.toJSONString(exceptionMap);
        return format("ERROR", traceId, module, message + " | 异常信息:%s", exceptionMsg);
    }

    /**
     * 敏感信息脱敏
     */
    private String maskSensitiveInfo(String message) {
        if (StringUtil.isBlank(message)) {
            return message;
        }
        // 手机号脱敏
        message = message.replaceAll("(1[3-9]\\d{9})", "$1".replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
        // 身份证脱敏
        message = message.replaceAll("([1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx])",
                "$1".replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2"));
        // 银行卡脱敏
        message = message.replaceAll("(\\d{16,19})", "$1".replaceAll("(\\d{4})\\d+(\\d{4})", "$1****$2"));
        return message;
    }

    /**
     * 获取异常堆栈信息
     */
    private String getStackTrace(Throwable e) {
        StringBuilder sb = new StringBuilder();
        for (StackTraceElement element : e.getStackTrace()) {
            sb.append(element.toString()).append("\n");
        }
        return sb.toString();
    }
}

2. 监控工具

2.1 MetricsUtil(自定义指标工具)

java 复制代码
package com.xxx.common.monitor.util;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.tag.Tag;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * 自定义指标工具(适配Prometheus,记录接口耗时、调用次数)
 * 常用度:90%
 * 所属层级:通用层(控制/业务)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Component
public class MetricsUtil {

    @Autowired
    private MeterRegistry meterRegistry;

    @Autowired(required = false)
    private PrometheusMeterRegistry prometheusMeterRegistry;

    /**
     * 计数器缓存(避免重复创建)
     */
    private final Map<String, Counter> counterCache = new ConcurrentHashMap<>();

    /**
     * 计时器缓存
     */
    private final Map<String, Timer> timerCache = new ConcurrentHashMap<>();

    /**
     * 通用标签:应用名称
     */
    private static final String APP_TAG_KEY = "app";

    /**
     * 通用标签:接口名称
     */
    private static final String API_TAG_KEY = "api";

    /**
     * 通用标签:结果(success/fail)
     */
    private static final String RESULT_TAG_KEY = "result";

    /**
     * 递增计数器(接口调用次数)
     */
    public void incrementApiCounter(String apiName, boolean success) {
        String counterName = "api_call_count";
        String result = success ? "success" : "fail";

        // 构建标签
        Tag appTag = Tag.of(APP_TAG_KEY, getAppName());
        Tag apiTag = Tag.of(API_TAG_KEY, apiName);
        Tag resultTag = Tag.of(RESULT_TAG_KEY, result);

        // 获取或创建计数器
        String cacheKey = counterName + "_" + apiName + "_" + result;
        Counter counter = counterCache.computeIfAbsent(cacheKey, k ->
                Counter.builder(counterName)
                        .description("接口调用次数")
                        .tags(appTag, apiTag, resultTag)
                        .register(meterRegistry)
        );

        // 递增
        counter.increment();
        log.debug("计数器[{}]递增,api={},result={}", counterName, apiName, result);
    }

    /**
     * 记录接口耗时
     */
    public void recordApiTimer(String apiName, long duration, TimeUnit timeUnit) {
        String timerName = "api_call_duration_seconds";

        // 构建标签
        Tag appTag = Tag.of(APP_TAG_KEY, getAppName());
        Tag apiTag = Tag.of(API_TAG_KEY, apiName);

        // 获取或创建计时器
        String cacheKey = timerName + "_" + apiName;
        Timer timer = timerCache.computeIfAbsent(cacheKey, k ->
                Timer.builder(timerName)
                        .description("接口调用耗时(秒)")
                        .tags(appTag, apiTag)
                        .register(meterRegistry)
        );

        // 记录耗时
        timer.record(duration, timeUnit);
        log.debug("计时器[{}]记录耗时,api={},duration={}{}", timerName, apiName, duration, timeUnit.name());
    }

    /**
     * 自定义计数器
     */
    public void incrementCounter(String counterName, String... tagValues) {
        Tag[] tags = buildTags(tagValues);
        String cacheKey = counterName + "_" + String.join("_", tagValues);

        Counter counter = counterCache.computeIfAbsent(cacheKey, k ->
                Counter.builder(counterName)
                        .tags(tags)
                        .register(meterRegistry)
        );
        counter.increment();
    }

    /**
     * 构建标签(键值对形式,如:"key1","value1","key2","value2")
     */
    private Tag[] buildTags(String... tagValues) {
        if (tagValues == null || tagValues.length % 2 != 0) {
            log.warn("标签参数必须为键值对形式");
            return new Tag[0];
        }

        Tag[] tags = new Tag[tagValues.length / 2];
        for (int i = 0; i < tagValues.length; i += 2) {
            tags[i / 2] = Tag.of(tagValues[i], tagValues[i + 1]);
        }
        // 添加默认应用标签
        return Arrays.copyOf(tags, tags.length + 1, Tag[].class);
    }

    /**
     * 获取应用名称(从环境变量/配置中读取)
     */
    private String getAppName() {
        return System.getProperty("spring.application.name", "unknown-app");
    }

    /**
     * 导出Prometheus指标(用于自定义暴露)
     */
    public String exportPrometheusMetrics() {
        if (prometheusMeterRegistry == null) {
            log.warn("未配置PrometheusMeterRegistry,无法导出指标");
            return "";
        }
        return prometheusMeterRegistry.scrape();
    }
}

// 接口耗时监控AOP(示例)
package com.xxx.common.monitor.aop;

import com.xxx.common.monitor.util.MetricsUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 接口耗时监控AOP
 * 常用度:90%
 * 所属层级:控制层(AOP)
 */
@Slf4j
@Aspect
@Component
public class ApiMetricsAOP {

    @Autowired
    private MetricsUtil metricsUtil;

    /**
     * 切点:所有Controller层方法
     */
    @Pointcut("within(com.xxx.api.controller..*)")
    public void apiMetricsPointcut() {
    }

    @Around("apiMetricsPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String apiName = joinPoint.getSignature().toShortString();
        long startTime = System.currentTimeMillis();
        boolean success = true;

        try {
            return joinPoint.proceed();
        } catch (Exception e) {
            success = false;
            throw e;
        } finally {
            // 记录调用次数
            metricsUtil.incrementApiCounter(apiName, success);
            // 记录耗时
            long duration = System.currentTimeMillis() - startTime;
            metricsUtil.recordApiTimer(apiName, duration, TimeUnit.MILLISECONDS);
        }
    }
}

2.2 HealthCheckUtil(健康检查工具)

java 复制代码
package com.xxx.common.monitor.util;

import com.xxx.common.enums.ErrorCodeEnum;
import com.xxx.common.exception.SystemException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

/**
 * 健康检查工具(自定义检查项:Redis/DB连接状态)
 * 常用度:90%
 * 所属层级:通用层(DAO/业务)
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Component
public class HealthCheckUtil {

    @Autowired(required = false)
    private DataSource dataSource;

    @Autowired(required = false)
    private JdbcTemplate jdbcTemplate;

    @Autowired(required = false)
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 执行全量健康检查
     */
    public Map<String, HealthCheckResult> checkAll() {
        Map<String, HealthCheckResult> resultMap = new HashMap<>();

        // 数据库检查
        resultMap.put("database", checkDatabase());

        // Redis检查
        resultMap.put("redis", checkRedis());

        // 自定义检查项(如:MinIO、消息队列等)
        resultMap.put("custom", checkCustom());

        return resultMap;
    }

    /**
     * 数据库健康检查
     */
    public HealthCheckResult checkDatabase() {
        HealthCheckResult result = new HealthCheckResult();
        result.setName("database");

        try {
            if (dataSource == null) {
                result.setHealthy(false);
                result.setMessage("数据源未配置");
                return result;
            }

            // 测试数据库连接
            try (Connection conn = dataSource.getConnection()) {
                if (conn.isValid(3)) {
                    // 执行简单查询
                    jdbcTemplate.queryForObject("SELECT 1", Integer.class);
                    result.setHealthy(true);
                    result.setMessage("数据库连接正常");
                } else {
                    result.setHealthy(false);
                    result.setMessage("数据库连接无效");
                }
            } catch (SQLException e) {
                result.setHealthy(false);
                result.setMessage("数据库连接失败:" + e.getMessage());
                log.error("数据库健康检查失败", e);
            }
        } catch (Exception e) {
            result.setHealthy(false);
            result.setMessage("数据库检查异常:" + e.getMessage());
            log.error("数据库健康检查异常", e);
        }

        return result;
    }

    /**
     * Redis健康检查
     */
    public HealthCheckResult checkRedis() {
        HealthCheckResult result = new HealthCheckResult();
        result.setName("redis");

        try {
            if (redisTemplate == null) {
                result.setHealthy(false);
                result.setMessage("RedisTemplate未配置");
                return result;
            }

            // 测试Redis连接
            String ping = redisTemplate.getConnectionFactory().getConnection().ping();
            if ("PONG".equals(ping)) {
                result.setHealthy(true);
                result.setMessage("Redis连接正常");
            } else {
                result.setHealthy(false);
                result.setMessage("Redis ping响应异常:" + ping);
            }
        } catch (Exception e) {
            result.setHealthy(false);
            result.setMessage("Redis连接失败:" + e.getMessage());
            log.error("Redis健康检查失败", e);
        }

        return result;
    }

    /**
     * 自定义健康检查(示例)
     */
    public HealthCheckResult checkCustom() {
        HealthCheckResult result = new HealthCheckResult();
        result.setName("custom");

        try {
            // 自定义检查逻辑(如:检查第三方接口、文件存储等)
            result.setHealthy(true);
            result.setMessage("自定义检查通过");
        } catch (Exception e) {
            result.setHealthy(false);
            result.setMessage("自定义检查失败:" + e.getMessage());
            log.error("自定义健康检查失败", e);
        }

        return result;
    }

    /**
     * 快速健康检查(仅检查核心组件)
     */
    public boolean quickCheck() {
        HealthCheckResult dbResult = checkDatabase();
        HealthCheckResult redisResult = checkRedis();
        return dbResult.isHealthy() && redisResult.isHealthy();
    }

    /**
     * 健康检查结果DTO
     */
    public static class HealthCheckResult {
        /**
         * 检查项名称
         */
        private String name;

        /**
         * 是否健康
         */
        private boolean healthy;

        /**
         * 检查消息
         */
        private String message;

        /**
         * 检查时间戳(毫秒)
         */
        private long timestamp = System.currentTimeMillis();

        // Getter & Setter
        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public boolean isHealthy() {
            return healthy;
        }

        public void setHealthy(boolean healthy) {
            this.healthy = healthy;
        }

        public String getMessage() {
            return message;
        }

        public void setMessage(String message) {
            this.message = message;
        }

        public long getTimestamp() {
            return timestamp;
        }

        public void setTimestamp(long timestamp) {
            this.timestamp = timestamp;
        }
    }
}

// 健康检查接口(暴露HTTP端点)
package com.xxx.api.controller;

import com.xxx.common.monitor.util.HealthCheckUtil;
import com.xxx.common.web.response.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * 健康检查接口
 * 常用度:90%
 * 所属层级:控制层
 */
@RestController
@RequestMapping("/actuator/custom/health")
public class HealthCheckController {

    @Autowired
    private HealthCheckUtil healthCheckUtil;

    @GetMapping("/all")
    public ApiResponse<Map<String, HealthCheckUtil.HealthCheckResult>> checkAll() {
        return ApiResponse.success(healthCheckUtil.checkAll());
    }

    @GetMapping("/quick")
    public ApiResponse<Boolean> quickCheck() {
        return ApiResponse.success(healthCheckUtil.quickCheck());
    }

    @GetMapping("/database")
    public ApiResponse<HealthCheckUtil.HealthCheckResult> checkDatabase() {
        return ApiResponse.success(healthCheckUtil.checkDatabase());
    }

    @GetMapping("/redis")
    public ApiResponse<HealthCheckUtil.HealthCheckResult> checkRedis() {
        return ApiResponse.success(healthCheckUtil.checkRedis());
    }
}

2.3 ExceptionLogUtil(异常日志工具)

java 复制代码
package com.xxx.common.monitor.util;

import com.xxx.common.exception.BusinessException;
import com.xxx.common.exception.ParamInvalidException;
import com.xxx.common.exception.SystemException;
import com.xxx.common.util.TraceIdUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

/**
 * 异常日志工具(统一记录异常,支持告警通知)
 * 常用度:90%
 * 所属层级:通用层
 *
 * @author xxx
 * @date 2025-11-30
 */
@Slf4j
@Component
public class ExceptionLogUtil {

    @Autowired
    private LogFormatter logFormatter;

    /**
     * 记录业务异常
     */
    public void recordBusinessException(BusinessException e, HttpServletRequest request) {
        Map<String, Object> exceptionInfo = buildExceptionInfo(e, request);
        exceptionInfo.put("exceptionType", "BUSINESS");

        // 输出日志
        log.warn(logFormatter.formatExceptionLog(
                TraceIdUtil.getTraceId(),
                "BUSINESS_EXCEPTION",
                "业务异常",
                e
        ));

        // 扩展:业务异常一般不触发告警,可根据错误码配置特殊告警规则
        if (e.getErrorCode().getCode() >= 600 && e.getErrorCode().getCode() <= 699) {
            sendAlarm(exceptionInfo, "WARN");
        }
    }

    /**
     * 记录参数异常
     */
    public void recordParamException(ParamInvalidException e, HttpServletRequest request) {
        Map<String, Object> exceptionInfo = buildExceptionInfo(e, request);
        exceptionInfo.put("exceptionType", "PARAM");

        log.warn(logFormatter.formatExceptionLog(
                TraceIdUtil.getTraceId(),
                "PARAM_EXCEPTION",
                "参数异常",
                e
        ));

        // 参数异常一般不告警
    }

    /**
     * 记录系统异常
     */
    public void recordSystemException(SystemException e, HttpServletRequest request) {
        Map<String, Object> exceptionInfo = buildExceptionInfo(e, request);
        exceptionInfo.put("exceptionType", "SYSTEM");

        log.error(logFormatter.formatExceptionLog(
                TraceIdUtil.getTraceId(),
                "SYSTEM_EXCEPTION",
                "系统异常",
                e
        ));

        // 系统异常触发告警
        sendAlarm(exceptionInfo, "ERROR");
    }

    /**
     * 记录未知异常
     */
    public void recordUnknownException(Exception e, HttpServletRequest request) {
        Map<String, Object> exceptionInfo = buildExceptionInfo(e, request);
        exceptionInfo.put("exceptionType", "UNKNOWN");

        log.error(logFormatter.formatExceptionLog(
                TraceIdUtil.getTraceId(),
                "UNKNOWN_EXCEPTION",
                "未知异常",
                e
        ));

        // 未知异常触发告警
        sendAlarm(exceptionInfo, "ERROR");
    }

    /**
     * 构建异常信息
     */
    private Map<String, Object> buildExceptionInfo(Throwable e, HttpServletRequest request) {
        Map<String, Object> exceptionInfo = new HashMap<>();
        exceptionInfo.put("traceId", TraceIdUtil.getTraceId());
        exceptionInfo.put("timestamp", LocalDateTime.now());
        exceptionInfo.put("exceptionClass", e.getClass().getName());
        exceptionInfo.put("exceptionMessage", e.getMessage());

        if (request != null) {
            exceptionInfo.put("requestURI", request.getRequestURI());
            exceptionInfo.put("requestMethod", request.getMethod());
            exceptionInfo.put("clientIp", getClientIp(request));
            exceptionInfo.put("userAgent", request.getHeader("User-Agent"));
        }

        // 记录异常堆栈(非生产环境)
        if (!isProdEnv()) {
            StringBuilder stackTrace = new StringBuilder();
            for (StackTraceElement element : e.getStackTrace()) {
                stackTrace.append(element.toString()).append("\n");
            }
            exceptionInfo.put("stackTrace", stackTrace.toString());
        }

        return exceptionInfo;
    }

    /**
     * 发送告警通知(示例:邮件/钉钉/企业微信)
     */
    private void sendAlarm(Map<String, Object> exceptionInfo, String level) {
        // 此处可实现告警逻辑,如:
        // 1. 钉钉机器人推送
        // 2. 企业微信机器人推送
        // 3. 邮件告警
        // 4. 短信告警(紧急)

        log.info("发送{}级告警:{}", level, exceptionInfo);
        // 示例:DingDingAlarmUtil.send(exceptionInfo, level);
    }

    /**
     * 判断是否为生产环境
     */
    private boolean isProdEnv() {
        String env = System.getProperty("spring.profiles.active", "dev");
        return "prod".equals(env);
    }

    /**
     * 获取客户端IP
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

大厂规范

  1. 包名规范 :统一使用 com.xxx.模块.子模块 格式,如 com.xxx.common.configcom.xxx.common.util
  2. 命名规范:类名使用大驼峰,方法名/变量名使用小驼峰,常量全大写下划线分隔;
  3. 异常规范:区分业务/系统/参数异常,生产环境隐藏堆栈,异常信息友好;
  4. 日志规范:全链路TraceId、结构化输出、敏感信息脱敏;
  5. 安全规范:防XSS/SQL注入、接口签名、幂等、防刷、敏感信息加密/脱敏;
  6. 性能规范:线程池防OOM、本地缓存减少DB查询、分页限制最大条数。
相关推荐
私人珍藏库2 小时前
[Windows] 隐写者 SteganographierGUI 1.3.8
windows·pc·工具·软件
seven_7678230985 小时前
MateChat MCP(模型上下文协议)深入剖析:从协议原理到自定义工具实战
工具·devui·mcp·matechat
私人珍藏库1 天前
[Android] 轻小说文库(1.23)
android·app·安卓·工具
xixixi777771 天前
了解一个开源日志平台——Elastic Stack
网络·安全·日志·工具
bin91532 天前
当AI化身Git管家:初级C++开发者的版本控制焦虑与创意逆袭——老码农的幽默生存指南
c++·人工智能·git·工具·ai工具
路人甲ing..5 天前
blender常用快捷键和BlenderKit使用
教程·blender·工具·快捷键·光学
sean90811 天前
git filter-repo(优秀的 git repo 历史重写工具) 实战
git·repo·工具
xixixi7777721 天前
攻击链重构的具体实现思路和分析报告
开发语言·python·安全·工具·攻击链
bin915323 天前
PHP文档保卫战:AI自动生成下的创意守护与反制指南
开发语言·人工智能·php·工具·ai工具