Loading

基于Easy Excel的表头校验工具

ExcelHeaderValidator

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.event.AnalysisEventListener;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * Excel表头校验工具类
 * 用于校验上传的Excel文件表头是否与DTO中定义的表头一致
 *
 * @author Ryan
 */
@Slf4j
public class ExcelHeaderValidator {

    /**
     * 创建一个带表头校验的数据监听器
     *
     * @param modelClass       DTO模型类
     * @param templateTypeName 模板类型名称(用于错误提示)
     * @param dataConsumer     数据消费者,处理每一行数据
     * @param <T>              DTO类型
     * @return 带表头校验的监听器
     */
    public static <T> AnalysisEventListener<T> createValidationListener(
            Class<T> modelClass,
            String templateTypeName,
            Consumer<T> dataConsumer) {

        Map<Integer, String> expectedHeaders = getExpectedHeaders(modelClass);
        return new ValidationHeaderListener<>(expectedHeaders, templateTypeName, dataConsumer);
    }

    /**
     * 从DTO类中提取期望的表头
     *
     * @param modelClass DTO模型类
     * @return 表头索引与表头名称的映射
     */
    public static Map<Integer, String> getExpectedHeaders(Class<?> modelClass) {
        // 使用字段在类中的声明顺序作为索引,而不是annotation.index()
        // 因为当注解未指定index时,默认值(-1或Integer.MAX_VALUE)会导致重复key异常
        List<Field> fields = Arrays.stream(modelClass.getDeclaredFields())
                .filter(field -> field.isAnnotationPresent(ExcelProperty.class))
                .collect(Collectors.toList());

        Map<Integer, String> result = new java.util.LinkedHashMap<>();
        for (int i = 0; i < fields.size(); i++) {
            Field field = fields.get(i);
            ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
            String headerName = annotation.value().length > 0 ? annotation.value()[0] : field.getName();
            result.put(i, headerName);
        }
        return result;
    }

    /**
     * 按顺序获取对象中每个字段的值
     *
     * @param data 数据对象
     * @param <T>  对象类型
     * @return 字段索引与字段值的映射(值转换为String)
     */
    public static <T> Map<Integer, String> getFieldValues(T data) {
        if (data == null) {
            return new java.util.LinkedHashMap<>();
        }

        List<Field> fields = Arrays.stream(data.getClass().getDeclaredFields())
                .collect(Collectors.toList());

        Map<Integer, String> result = new java.util.LinkedHashMap<>();
        for (int i = 0; i < fields.size(); i++) {
            Field field = fields.get(i);
            field.setAccessible(true);
            try {
                Object value = field.get(data);
                result.put(i, value != null ? value.toString() : "");
            } catch (IllegalAccessException e) {
                log.error("获取字段值失败: {}", field.getName(), e);
                result.put(i, "");
            }
        }
        return result;
    }

}

ValidationHeaderListener

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.util.ConverterUtils;
import com.szyk.car.common.web.config.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

/**
 * <p> 导入表头校验监听器</p>
 * <p> Create Time: 2026/2/4 </p>
 *
 * @author Ryan
 */
@Slf4j
public class ValidationHeaderListener<T> extends AnalysisEventListener<T> {

    private final Map<Integer, String> expectedHeaders;
    private final String templateTypeName;
    private final Consumer<T> dataConsumer;
    private boolean headerValidated = false;

    public ValidationHeaderListener(Map<Integer, String> expectedHeaders, String templateTypeName, Consumer<T> dataConsumer) {
        this.expectedHeaders = expectedHeaders;
        this.templateTypeName = templateTypeName;
        this.dataConsumer = dataConsumer;
    }

    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        Map<Integer, String> convertHeadMap = ConverterUtils.convertToStringMap(headMap, context);
        // 保存实际表头,供 invoke 方法中的兜底校验使用
        if (!headerValidated) {
            if (ObjectUtil.isEmpty(convertHeadMap)) {
                throw new BusinessException("Excel文件表头为空");
            }
            validateHeaders(convertHeadMap, expectedHeaders, templateTypeName);
            headerValidated = true;
        }
    }

    @Override
    public void invoke(T data, AnalysisContext context) {
        // 双重保障:确保在处理第一行数据前表头已校验
        if (!headerValidated) {
            // 通过readRowHolder获取当前行索引,第一行数据(除去表头)的索引为1
            Integer currentRowIndex = context.readRowHolder().getRowIndex();
            if (currentRowIndex != null && currentRowIndex == 1) {
                // 从data的类定义中获取期望表头
                Map<Integer, String> expectedHeadersFromClass = ExcelHeaderValidator.getFieldValues(data);
                // 获取实际表头进行校验
                if (ObjectUtil.isEmpty(expectedHeadersFromClass)) {
                    throw new BusinessException("Excel文件表头为空");
                }
                validateHeaders(expectedHeadersFromClass, expectedHeaders, templateTypeName);
                headerValidated = true;
                return;
            }
        }
        dataConsumer.accept(data);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
    }


    /**
     * 校验表头是否匹配
     *
     * @param actualHeaders    实际表头
     * @param expectedHeaders  期望表头
     * @param templateTypeName 模板类型名称
     * @throws IllegalArgumentException 表头不匹配时抛出异常
     */
    private static void validateHeaders(Map<Integer, String> actualHeaders,
                                        Map<Integer, String> expectedHeaders,
                                        String templateTypeName) {
        List<String> errors = new ArrayList<>();

        // 检查缺少的列
        for (Map.Entry<Integer, String> entry : expectedHeaders.entrySet()) {
            String actualHeader = actualHeaders.get(entry.getKey());
            if (actualHeader == null) {
                errors.add(String.format("缺少第%d列【%s】", entry.getKey() + 1, entry.getValue()));
            } else if (!actualHeader.equals(entry.getValue())) {
                errors.add(String.format("第%d列应为【%s】,实际为【%s】",
                        entry.getKey() + 1, entry.getValue(), actualHeader));
            }
        }

        // 检查多余的列
        for (Map.Entry<Integer, String> entry : actualHeaders.entrySet()) {
            if ("异常报文".equals(entry.getValue())){
                continue;
            }
            String expectedHeader = expectedHeaders.get(entry.getKey());
            if (expectedHeader == null) {
                errors.add(String.format("存在多余的第%d列【%s】", entry.getKey() + 1, entry.getValue()));
            }
        }

        if (!errors.isEmpty()) {
            String errorMsg = String.format("%s模板不正确:%s", templateTypeName, String.join(";", errors));
            log.error(errorMsg);
            throw new BusinessException(String.format("%s模板不正确!", templateTypeName));
        }
    }
}
posted @ 2026-02-05 14:58  IamHzc  阅读(36)  评论(0)    收藏  举报