250615-导入功能前后端代码实现与使用规范.md 31 KB

Excel 导入功能前后端代码实现与使用规范

本文档基于项目中"个人信息"和"企业信息"模块的导入功能实现进行总结,后续开发新模块的导入功能时,按照本文档的步骤和代码模板照写即可。


一、整体架构概览

导入功能的整体流程如下:

前端上传 Excel → 后端 Controller 接收 → AutoPoi 解析 Excel → Service 逐行处理
→ 字典反向翻译 → 唯一键去重 → 必填校验 → 批量保存 → 返回校验结果

涉及的文件分层:

层级 文件 职责
前端 - 公共组件 src/components/CustomImportModal/src/CustomImportModal.vue 通用的导入弹窗组件
前端 - API 层 src/views/{module}/{Entity}Info.api.ts 定义导入/模板下载接口URL
前端 - 列表页 src/views/{module}/{Entity}InfoList.vue 集成 CustomImportModal 组件
后端 - DTO personal/dto/ImportResultItem.java 导入校验结果的数据结构(通用)
后端 - Controller {module}/controller/{Entity}InfoController.java 接收文件上传、提供模板下载
后端 - Service接口 {module}/service/I{Entity}InfoService.java 声明 importExcelData 方法
后端 - Service实现 {module}/service/impl/{Entity}InfoServiceImpl.java 核心导入逻辑

二、后端实现详解

2.1 ImportResultItem — 导入校验结果通用 DTO

文件位置: personal/dto/ImportResultItem.java

package org.jeecg.modules.zjrs.personal.dto;

import lombok.Data;
import java.util.Map;

/**
 * 导入结果行数据:包含原始Excel列值 + 错误信息(如有)
 */
@Data
public class ImportResultItem {
    /** Excel行号 */
    private int rowNum;
    /** 该行各列的原始文本值:Excel列名 → 值 */
    private Map<String, String> rowData;
    /** 错误信息:null或空表示该行校验通过 */
    private String errorMsg;
}

此 DTO 是通用结构,所有模块共用。rowData 存放该行所有 Excel 列的原始值(key 为 Excel 列名 @Excel.name),errorMsg 为 null 表示该行校验通过。


2.2 Service 接口 — 声明导入方法

文件位置: {module}/service/I{Entity}InfoService.java

在接口中新增 importExcelData 方法声明:

/**
 * 批量导入Excel数据
 * 对解析后的数据进行:字典文本→字典码反向翻译、唯一键去重、必填字段校验、批量保存
 *
 * @param list Excel解析后的数据列表
 * @return 每行的导入结果(含原始列值 + 错误信息),当全部无错误时内部会执行批量保存
 */
List<ImportResultItem> importExcelData(List<{Entity}Info> list);

示例参考:

  • IPersonalInfoService.java (service/IPersonalInfoService.java)
  • IEnterpriseInfoService.java (service/IEnterpriseInfoService.java)

2.3 Service 实现 — 导入核心逻辑(最重要)

文件位置: {module}/service/impl/{Entity}InfoServiceImpl.java

核心方法 importExcelData 执行以下步骤:

步骤0:定义常量 → 步骤1:字典文本→字典码反向翻译映射 → 步骤2:唯一键去重(Excel内部+数据库)
→ 步骤3:逐行校验(字典翻译 + 必填校验 + 补充字段)→ 步骤4:有错误则返回错误明细,无错误则批量保存

2.3.1 必须定义的三个常量

在 ServiceImpl 类中定义三个全局常量:

// 常量1:导入时需要进行 文本→字典码 反向翻译的字段:entity字段名 → 字典编码
// 只有字典字段(前端是下拉选择而非自由输入的字段)才需要配置此处
private static final Map<String, String> IMPORT_DICT_FIELD_MAP = new LinkedHashMap<>();

static {
    IMPORT_DICT_FIELD_MAP.put("gender", "Gender");           // entity字段名 → 字典编码
    IMPORT_DICT_FIELD_MAP.put("education", "Education");
    // ... 根据实际业务字段添加
}

// 常量2:导入模板的字段名列表,顺序必须与 Controller 中 importTemplate 的 fieldNames 一致
private static final List<String> IMPORT_FIELD_NAMES = Arrays.asList(
    "idType", "idNumber", "fullName", "gender", "birthDate"
    // ... 所有模板列字段(包括必填和非必填),顺序与模板导出时的列顺序严格一致
);

// 常量3:日期格式化器
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

注意IMPORT_FIELD_NAMES 的顺序必须与 Controller 中 importTemplate 方法里的 fieldNames 数组顺序完全一致,否则错误反馈时列名和列值会错位。

2.3.2 核心方法完整代码模板

@Override
public List<ImportResultItem> importExcelData(List<{Entity}Info> list) {
    // ========== 步骤1:批量查询所有字典项,构建 文本→字典码 的反向映射表 ==========
    Map<String, Map<String, String>> dictTextToCodeMap = new HashMap<>();
    for (Map.Entry<String, String> entry : IMPORT_DICT_FIELD_MAP.entrySet()) {
        String dictCode = entry.getValue();
        if (!dictTextToCodeMap.containsKey(dictCode)) {
            // 从字典表查询,构建 中文文本→字典码 映射
            List<DictModel> items = dictionaryItemMapper.queryDictItemsByCode(dictCode);
            Map<String, String> textToCode = new HashMap<>();
            for (DictModel item : items) {
                textToCode.put(item.getText(), item.getValue());
            }
            dictTextToCodeMap.put(dictCode, textToCode);
        }
    }

    // ========== 步骤2:唯一键去重(Excel内部 + 数据库已存在)==========
    // 以"统一社会信用代码"或"证件号码"作为唯一键,检查Excel中是否有重复、数据库中是否已存在
    Map<String, List<Integer>> uniqueKeyPositions = new HashMap<>();
    for (int i = 0; i < list.size(); i++) {
        String uniqueKey = list.get(i).getUniqueKeyField(); // 替换为你的唯一键getter
        if (oConvertUtils.isNotEmpty(uniqueKey)) {
            uniqueKeyPositions.computeIfAbsent(uniqueKey, k -> new ArrayList<>()).add(i);
        }
    }

    // 检查Excel内部重复
    Map<String, String> uniqueKeyDuplicateMsg = new HashMap<>();
    for (Map.Entry<String, List<Integer>> entry : uniqueKeyPositions.entrySet()) {
        if (entry.getValue().size() > 1) {
            String msg = "唯一键【" + entry.getKey() + "】在数据中多次出现(行:"
                    + entry.getValue().stream().map(pos -> String.valueOf(pos + 3))
                            .collect(Collectors.joining("、"))
                    + ")";
            for (Integer pos : entry.getValue()) {
                uniqueKeyDuplicateMsg.put(String.valueOf(pos), msg);
            }
        }
    }

    // 检查数据库是否已存在
    for (Map.Entry<String, List<Integer>> entry : uniqueKeyPositions.entrySet()) {
        String uniqueKey = entry.getKey();
        // 如果Excel内已经重复,不再重复报数据库重复
        if (uniqueKeyDuplicateMsg.containsKey(String.valueOf(entry.getValue().get(0)))) {
            continue;
        }
        LambdaQueryWrapper<{Entity}Info> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq({Entity}Info::getUniqueKeyField, uniqueKey); // 替换
        long dbCount = this.count(queryWrapper);
        if (dbCount > 0) {
            for (Integer pos : entry.getValue()) {
                uniqueKeyDuplicateMsg.put(String.valueOf(pos),
                        "唯一键【" + uniqueKey + "】已在系统中存在");
            }
        }
    }

    // ========== 步骤3:逐行校验与字段处理 ==========
    List<ImportResultItem> resultItems = new ArrayList<>();
    boolean hasAnyError = false;

    for (int i = 0; i < list.size(); i++) {
        {Entity}Info item = list.get(i);
        int rowNum = i + 3; // Excel行号:标题1行 + 表头1行 = 前2行为头,数据从第3行开始

        // 翻译前捕获原始Excel列值,用于错误反馈时展示原数据
        Map<String, String> rowData = captureOriginalValues(item);
        List<String> rowErrors = new ArrayList<>();

        // 唯一键重复校验
        String dupMsg = uniqueKeyDuplicateMsg.get(String.valueOf(i));
        if (dupMsg != null) {
            rowErrors.add(dupMsg);
        }

        try {
            // 3.1 字典文本→字典码 反向翻译
            for (Map.Entry<String, String> fieldEntry : IMPORT_DICT_FIELD_MAP.entrySet()) {
                String fieldName = fieldEntry.getKey();
                String dictCode = fieldEntry.getValue();
                String textValue = (String) getPropertyValue(item, fieldName);
                if (oConvertUtils.isNotEmpty(textValue)) {
                    Map<String, String> textToCode = dictTextToCodeMap.get(dictCode);
                    String codeValue = textToCode.get(textValue.trim());
                    if (codeValue != null) {
                        setPropertyValue(item, fieldName, codeValue);
                    } else {
                        // 字典中找不到对应值 → 校验失败
                        rowErrors.add("【" + getFieldExcelName(fieldName) + "】值\""
                                + textValue + "\"在字典\"" + dictCode + "\"中不存在");
                    }
                }
            }

            // 3.2 校验必填字段(利用已有的 validateRequiredFields 方法)
            if (rowErrors.isEmpty()) {
                try {
                    validateRequiredFields(item);
                } catch (JeecgBootBizTipException e) {
                    rowErrors.add(e.getMessage());
                }
            }

            // 3.3 补充成对字段、设置默认值(根据业务需要)
            if (rowErrors.isEmpty()) {
                // 例如:设置数据来源为"导入"
                item.setDataSource("2");
                // 其他业务字段补充...
            }
        } catch (Exception e) {
            rowErrors.add(e.getMessage());
        }

        // 构建该行的校验结果
        ImportResultItem resultItem = new ImportResultItem();
        resultItem.setRowNum(rowNum);
        resultItem.setRowData(rowData); // 原始Excel列值
        if (!rowErrors.isEmpty()) {
            resultItem.setErrorMsg(String.join(";", rowErrors));
            hasAnyError = true;
            resultItems.add(resultItem);
        }
        // 没错误的不加入 resultItems(减少返回数据量)
    }

    // ========== 步骤4:有错误则不保存,全部通过则批量保存 ==========
    if (hasAnyError) {
        return resultItems;
    }

    long start = System.currentTimeMillis();
    this.saveBatch(list);
    log.info("导入消耗时间" + (System.currentTimeMillis() - start) + "毫秒");
    return resultItems;
}

2.3.3 必须实现的三个辅助方法

/**
 * 根据字段名获取对应的Excel列名(反射读取 @Excel 注解的 name 属性)
 * 用于错误提示时展示用户能看懂的列名,而非Java驼峰字段名
 */
private String getFieldExcelName(String fieldName) {
    try {
        java.lang.reflect.Field field = {Entity}Info.class.getDeclaredField(fieldName);
        org.jeecgframework.poi.excel.annotation.Excel excel =
                field.getAnnotation(org.jeecgframework.poi.excel.annotation.Excel.class);
        if (excel != null) {
            return excel.name();
        }
    } catch (NoSuchFieldException e) {
        // ignore
    }
    return fieldName; // 兜底:返回字段名
}

/**
 * 捕获原始Excel列值,以Excel列名作为key
 * 用于校验失败时在错误详情弹窗中展示用户填写的原始数据
 */
private Map<String, String> captureOriginalValues({Entity}Info item) {
    Map<String, String> rowData = new LinkedHashMap<>();
    for (String fieldName : IMPORT_FIELD_NAMES) {
        String excelName = getFieldExcelName(fieldName);
        try {
            Object value = getPropertyValue(item, fieldName);
            if (value == null) {
                rowData.put(excelName, "");
            } else if (value instanceof java.util.Date) {
                rowData.put(excelName, DATE_FORMAT.format(value)); // 日期格式化
            } else {
                rowData.put(excelName, value.toString());
            }
        } catch (Exception e) {
            rowData.put(excelName, "");
        }
    }
    return rowData;
}

/**
 * 通过 getter 方法名反射读取属性值,兼容 @Accessors(chain = true) 的实体
 * 注意:这两个方法是通用的静态工具方法,可以抽取到公共工具类中复用
 */
private static Object getPropertyValue(Object bean, String propertyName) throws Exception {
    String getterName = "get" + Character.toUpperCase(propertyName.charAt(0))
            + propertyName.substring(1);
    return bean.getClass().getMethod(getterName).invoke(bean);
}

/**
 * 通过 setter 方法名反射设置属性值,兼容 @Accessors(chain = true) 的实体
 */
private static void setPropertyValue(Object bean, String propertyName, Object value)
        throws Exception {
    String setterName = "set" + Character.toUpperCase(propertyName.charAt(0))
            + propertyName.substring(1);
    bean.getClass().getMethod(setterName, value.getClass()).invoke(bean, value);
}

2.4 Controller — 接收导入请求与提供模板下载

文件位置: {module}/controller/{Entity}InfoController.java

2.4.1 导入Excel数据接口

@RequiresPermissions("{module}:{entityInfo}:importExcel")
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
    MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
    Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();

    for (Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) {
        MultipartFile file = entity.getValue();
        ImportParams params = new ImportParams();
        params.setTitleRows(1);   // 标题行数(模板第0行为标题行)
        params.setHeadRows(1);    // 表头行数(模板第1行为表头行)
        try {
            // AutoPoi 将 Excel 解析为 Entity 列表
            List<{Entity}Info> list = ExcelImportUtil.importExcel(
                    file.getInputStream(), {Entity}Info.class, params);

            // 调用 Service 进行校验与保存
            List<ImportResultItem> resultItems = {entity}InfoService.importExcelData(list);

            // 检查是否有行校验失败
            boolean hasError = false;
            for (ImportResultItem item : resultItems) {
                if (item.getErrorMsg() != null && !item.getErrorMsg().isEmpty()) {
                    hasError = true;
                    break;
                }
            }
            if (hasError) {
                // 返回 code=500 + result=校验错误行列表,前端会展示错误详情弹窗
                return Result.error("导入数据校验失败", resultItems);
            }
            return Result.ok("文件导入成功!数据行数:" + list.size());
        } catch (Exception e) {
            String msg = e.getMessage();
            log.error(msg, e);
            if (msg != null && msg.contains("Duplicate entry")) {
                return Result.error("文件导入失败:有重复数据!");
            } else {
                return Result.error("文件导入失败:" + e.getMessage());
            }
        } finally {
            try {
                file.getInputStream().close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return Result.error("文件导入失败!");
}

关键点

  • 返回 Result.error(msg, resultItems) 时,前端检测到 code=500result 是数组,就会打开错误详情弹窗
  • ImportParams.titleRowsheadRows 的值必须与导出的模板结构一致

2.4.2 下载导入模板接口

@RequiresPermissions("{module}:{entityInfo}:importExcel")
@RequestMapping(value = "/importTemplate")
public void importTemplate(HttpServletRequest request, HttpServletResponse response)
        throws Exception {
    String title = "{业务名}导入模板";
    ExportParams exportParams = new ExportParams(title, "导入模板", ExcelType.XSSF);

    // 定义模板的列(必填在前,非必填在后)
    String[] fieldNames = {"idType", "idNumber", "fullName", /* 所有列... */};

    // 构建一条示例数据
    List<{Entity}Info> dataList = new ArrayList<>(List.of(buildExampleData()));

    // AutoPoi 生成完整 workbook(含列头、样式、示例数据)
    Workbook workbook = ExcelExportUtil.exportExcel(
            exportParams, {Entity}Info.class, dataList, fieldNames);

    Sheet sheet = workbook.getSheetAt(0);
    // AutoPoi 带 ExportParams(title,...) 时:第0行为标题行,第1行为表头行
    Row headerRow = sheet.getRow(1);

    // 定义必填字段集合(与 Service 中 validateRequiredFields 保持一致)
    Set<String> requiredFields = new HashSet<>(Arrays.asList(
            "idType", "idNumber", "fullName" /* 必填字段列表 */
    ));

    // 必填字段表头加红色加粗样式
    CellStyle requiredStyle = workbook.createCellStyle();
    Font requiredFont = workbook.createFont();
    requiredFont.setColor(IndexedColors.RED.getIndex());
    requiredFont.setBold(true);
    requiredStyle.setFont(requiredFont);
    // 复制 AutoPoi 默认的边框/对齐等样式
    if (headerRow != null && headerRow.getCell(0) != null) {
        requiredStyle.cloneStyleFrom(headerRow.getCell(0).getCellStyle());
        requiredStyle.setFont(requiredFont);
    }

    for (int i = 0; i < fieldNames.length; i++) {
        Cell cell = headerRow.getCell(i);
        if (cell != null && requiredFields.contains(fieldNames[i])) {
            cell.setCellStyle(requiredStyle); // 必填字段表头标红
        }
    }

    // 写出响应
    response.setContentType(
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setHeader("Content-disposition",
            "attachment; filename=" + URLEncoder.encode(title + ".xlsx", "UTF-8"));
    workbook.write(response.getOutputStream());
    workbook.close();
}

/**
 * 构建模板的示例数据行
 * 字典字段填写中文文本(与字典表一致),导入时会自动反向翻译为字典码
 */
private {Entity}Info buildExampleData() {
    {Entity}Info example = new {Entity}Info();
    example.setFullName("张三");       // 自由文本字段
    example.setGender("男");           // 字典字段 → 填写中文标签文本
    example.setEducation("大学本科");   // 字典字段 → 填写中文标签文本
    // ... 所有模板列的示例数据
    return example;
}

注意fieldNames 数组的顺序必须与 ServiceImpl 中 IMPORT_FIELD_NAMES 常量完全一致。必填字段集合 requiredFields 要与 validateRequiredFields 方法中的校验逻辑一致。


三、前端实现详解

3.1 CustomImportModal 公共组件

文件位置: src/components/CustomImportModal/src/CustomImportModal.vue

该组件是通用的导入弹窗,所有模块可直接复用,无需修改组件源码。

组件功能

  1. 文件选择:支持 .xls.xlsx 格式,限制 2MB
  2. 模板下载:可配置下载按钮(传入 downloadUrl
  3. 上传导入:调用 importUrl 上传文件
  4. 错误展示:当后端返回 code=500 + result 数组时,自动弹出错误详情表格,展示每行的原始 Excel 数据和错误原因

Props 参数

参数 类型 必填 默认值 说明
auth String '' 权限标识,用于 v-auth 控制按钮显示
importUrl String 导入上传的接口URL
downloadUrl String '' 模板下载的接口URL
templateName String '下载导入模板' 模板下载按钮文本
buttonText String '导入' 触发按钮文本
buttonIcon String 'ant-design:import-outlined' 触发按钮图标
buttonType String 'primary' 触发按钮类型
success Function 导入成功后的回调函数

Events

事件名 说明 回调参数
success 导入成功后触发

3.2 API 层 — 定义导入接口URL

文件位置: src/views/{module}/{Entity}Info.api.ts

在 API 枚举和导出函数中添加:

enum Api {
  // ... 其他接口
  importExcel = '/enterprise/enterpriseInfo/importExcel',     // 导入上传URL
  importTemplate = '/enterprise/enterpriseInfo/importTemplate', // 模板下载URL
}

/**
 * 导入api(传给 CustomImportModal 的 importUrl)
 */
export const getImportUrl = Api.importExcel;

/**
 * 导入模板下载api(传给 CustomImportModal 的 downloadUrl)
 */
export const getImportTemplateUrl = Api.importTemplate;

3.3 列表页 — 集成 CustomImportModal

文件位置: src/views/{module}/{Entity}InfoList.vue

3.3.1 模板中引入组件

在表格标题区域(#tableTitle 插槽)中添加:

<CustomImportModal
  auth="{module}:{entityInfo}:importExcel"
  :importUrl="getImportUrl"
  :downloadUrl="getImportTemplateUrl"
  templateName="{业务名}导入模板"
  buttonText="导入"
  @success="handleSuccess"
/>

3.3.2 script 中导入

import { CustomImportModal } from '/@/components/CustomImportModal';
import { getImportUrl, getImportTemplateUrl } from './{Entity}Info.api';

不需要在 components 中注册(组件已通过 withInstall 全局注册)。


四、新模块接入步骤清单

按以下步骤操作,即可为新模块添加完整的导入功能:

后端(5步)

  1. 确认 Entity 的 @Excel 注解:实体类中需要用 @Excel(name = "列名") 标注字段,这样 AutoPoi 才能按列名解析 Excel,错误提示也能展示中文列名

  2. Service 接口添加方法

    List<ImportResultItem> importExcelData(List<{Entity}Info> list);
    
  3. ServiceImpl 实现 importExcelData

    • 定义 IMPORT_DICT_FIELD_MAP(字典字段映射)
    • 定义 IMPORT_FIELD_NAMES(模板列顺序,与第5步模板列顺序一致)
    • 定义 DATE_FORMAT
    • 照抄 2.3.2 节的代码模板,替换实体类名、唯一键字段
    • 照抄 2.3.3 节的三个辅助方法
  4. Controller 新增两个接口

    • POST /importExcel — 照抄 2.4.1 节代码模板
    • GET /importTemplate — 照抄 2.4.2 节代码模板,编写 buildExampleData() 方法
  5. 核对一致性

    • Controller 的 fieldNames 顺序 = ServiceImpl 的 IMPORT_FIELD_NAMES 顺序
    • Controller 的 requiredFields 集合 = ServiceImpl 的 validateRequiredFields 逻辑

前端(2步)

  1. API 层添加接口URL

    export const getImportUrl = Api.importExcel;
    export const getImportTemplateUrl = Api.importTemplate;
    
  2. 列表页引入 CustomImportModal

    <CustomImportModal
     auth="权限标识"
     :importUrl="getImportUrl"
     :downloadUrl="getImportTemplateUrl"
     templateName="模板名称"
     @success="handleSuccess"
    />
    

五、数据流转示意图

┌──────────────────────────────────────────────────────────────────────────┐
│ 前端                                                                      │
│                                                                           │
│  [导入按钮] → [选择Excel文件] → [开始导入]                                  │
│       │                              │                                    │
│       ▼                              ▼                                    │
│  [下载模板]                    POST /importExcel                          │
│  GET /importTemplate           上传 MultipartFile                          │
│                                                                           │
├──────────────────────────────────────────────────────────────────────────┤
│ 后端                                                                      │
│                                                                           │
│  Controller.importExcel()                                                 │
│       │                                                                   │
│       ▼                                                                   │
│  ExcelImportUtil.importExcel()  →  List<Entity>(AutoPoi解析,字典字段仍是中文)│
│       │                                                                   │
│       ▼                                                                   │
│  Service.importExcelData()                                                │
│       │                                                                   │
│       ├── 步骤1:批量查字典,构建 中文→字典码 映射                             │
│       ├── 步骤2:唯一键去重(Excel内部 + 数据库存在)                          │
│       ├── 步骤3:逐行校验                                                   │
│       │    ├── 字典文本→字典码 反向翻译                                      │
│       │    ├── 必填字段校验(调用 validateRequiredFields)                   │
│       │    └── 补充默认值(如 dataSource = "2")                             │
│       │                                                                   │
│       └── 步骤4:                                                          │
│            ├── 有错误 → return List<ImportResultItem>(不保存)              │
│            │          前端展示错误详情表格                                    │
│            └── 无错误 → saveBatch() 批量写入数据库                           │
│                                                                           │
└──────────────────────────────────────────────────────────────────────────┘

六、注意事项与常见问题

  1. 日期格式:Excel 中日期列请填写 yyyy-MM-dd 格式(如 1990-01-01),模板示例数据也使用此格式

  2. 字典字段:模板中字典列填写的是中文标签文本(如性别填"男"而不是字典码"1"),导入时会自动反向翻译为字典码存入数据库

  3. Excel 行号计算:数据从第3行开始(第0行标题 + 第1行表头),所以错误提示中的行号 = 列表索引 + 3

  4. 唯一键去重:如果业务的唯一键不止一个列,需要扩展步骤2的去重逻辑。如果业务没有唯一键约束,可以跳过步骤2

  5. 字段顺序一致性:Controller 的 fieldNames、ServiceImpl 的 IMPORT_FIELD_NAMES、Entity 的 @Excel 注解,三者的列顺序必须完全一致

  6. 权限标识:导入按钮和模板下载使用 {module}:{entityInfo}:importExcel 权限,Controller 方法也需要加 @RequiresPermissions

  7. 大量数据导入:超过 1000 行数据时,建议使用 saveBatch(list, 500) 分批次保存


七、涉及的核心文件清单

文件 作用
CustomImportModal.vue 前端公共导入弹窗组件
ImportResultItem.java 导入校验结果通用DTO
PersonalInfoController.java 个人信息导入Controller(参考实现)
EnterpriseInfoController.java 企业信息导入Controller(参考实现)
PersonalInfoServiceImpl.java 个人信息导入Service实现(参考实现)
EnterpriseInfoServiceImpl.java 企业信息导入Service实现(参考实现)
PersonalInfo.api.ts 个人信息API定义(参考实现)
EnterpriseInfo.api.ts 企业信息API定义(参考实现)
PersonalInfoList.vue 个人信息列表页(组件集成示例)
EnterpriseInfoList.vue 企业信息列表页(组件集成示例)
IPersonalInfoService.java 个人信息Service接口(接口声明示例)
IEnterpriseInfoService.java 企业信息Service接口(接口声明示例)