scaffold-import-export 导入导出模块

模块概述

scaffold-import-export(模块名 scaffold-ie)是基于 EasyExcel 的通用导入导出框架。采用 SPI 架构,业务模块只需实现 ImportDataHandlerExportDataHandler 接口,即可通过统一的模板配置实现 Excel 导入导出。支持导入预览(前 10 行 + 数据校验)、异步导出(线程池)、数据校验(必填、长度、正则)和模板管理。

功能列表

  • 模板管理:创建、更新、删除、查询导入导出模板,定义列配置(字段名、标签、类型、校验规则)
  • 导入预览:上传 Excel 后预览前 10 行数据并执行校验,返回错误信息
  • 执行导入:解析 Excel 数据,调用业务模块的 ImportDataHandler 处理
  • 异步导出:通过线程池异步执行导出,分页查询数据后生成 Excel 并上传到文件模块
  • 数据校验:支持必填、最大长度、正则表达式校验
  • 模板下载:根据模板配置生成空 Excel 模板供用户下载
  • 进度跟踪:导出任务记录处理进度(0-100)

状态字段规范

  • status 字段(模板)遵循布尔语义:1 = 启用,0 = 禁用
  • 注意:TaskStatusEnum 为多态状态机(待处理/处理中/成功/失败/部分成功),不遵循布尔语义

核心组件

类名 职责
Controller ImportExportController 导入导出任务接口,路径 /v1/ie/task
Controller TemplateController 模板管理接口,路径 /v1/ie/template
Service ImportExportTaskService 导入导出任务业务逻辑
Service TemplateService 模板管理业务逻辑
Core ExcelParser Excel 文件解析
Core ExcelGenerator Excel 文件生成
Core DataValidator 数据校验(必填、长度、正则)
Core AsyncExportExecutor 异步导出执行器,在线程池中执行导出任务
SPI ImportDataHandler 导入数据处理器接口
SPI ExportDataHandler 导出数据处理器接口
SPI ImportExportHandlerRegistry 处理器注册中心,按 businessType 自动关联
SPI ImportContext / ExportContext 导入/导出上下文
SPI ImportResult 导入结果(成功/失败行数、错误详情)
Entity IeTask 导入导出任务实体
Entity IeTemplate 模板实体
Config ImportExportAutoConfiguration 自动配置类
Config ImportExportProperties 配置属性类
Config IeThreadPoolConfig 导出线程池配置

配置参数

配置前缀:scaffold.ie

参数 类型 默认值 说明
enabled boolean true 模块开关
importBatchSize Integer 500 导入批量处理大小
exportBatchSize Integer 500 导出批量查询大小
maxRows Integer 10000 单次导入/导出最大行数
poolSize Integer 4 导出线程池核心线程数(最大线程数为核心线程数 * 2)

枚举值说明

枚举 说明
ColumnTypeEnum.STRING STRING 文本列
ColumnTypeEnum.NUMBER NUMBER 数字列
ColumnTypeEnum.DATE DATE 日期列
ColumnTypeEnum.DICT DICT 字典列
ColumnTypeEnum.BOOLEAN BOOLEAN 布尔列
TaskTypeEnum.IMPORT 1 导入任务
TaskTypeEnum.EXPORT 2 导出任务
TaskStatusEnum.PENDING 0 待处理
TaskStatusEnum.PROCESSING 1 处理中
TaskStatusEnum.SUCCESS 2 成功
TaskStatusEnum.FAILED 3 失败
TaskStatusEnum.PARTIAL 4 部分成功
TemplateTypeEnum.IMPORT 1 导入模板
TemplateTypeEnum.EXPORT 2 导出模板
TemplateTypeEnum.BOTH 3 双向模板

API 接口列表

导入导出任务 /v1/ie/task

方法 路径 说明
POST /importPreview 上传预览(前10行 + 校验),参数:templateId(Long)、file(MultipartFile)
POST /import 执行导入,参数:templateId(Long)、file(MultipartFile)、params(可选)
POST /export 触发异步导出,Body:ExportRequestDTO
GET /page 任务分页查询
GET /get/{id} 任务详情(含进度)

模板管理 /v1/ie/template

方法 路径 说明
POST /create 创建模板
POST /update 更新模板
POST /delete/{id} 删除模板
GET /get/{id} 模板详情
GET /page 模板分页查询
GET /downloadTemplate/{id} 下载导入模板(空 Excel 文件)

SPI 扩展点

ImportDataHandler 接口

public interface ImportDataHandler {
    /** 业务类型标识 */
    String getBusinessType();

    /** 执行导入 */
    ImportResult importData(List<Map<String, Object>> rows, ImportContext context);
}

ExportDataHandler 接口

public interface ExportDataHandler {
    /** 业务类型标识 */
    String getBusinessType();

    /** 查询导出数据 */
    List<Map<String, Object>> queryExportData(ExportContext context);
}

ImportContext 字段

字段 类型 说明
templateId Long 模板ID
businessType String 业务类型
params String 额外参数(JSON)
operatorId Long 操作人ID
operatorName String 操作人姓名

ExportContext 字段

字段 类型 说明
templateId Long 模板ID
businessType String 业务类型
params String 额外参数(JSON)
operatorId Long 操作人ID
selectedColumns List<String> 选中的导出列
pageNum int 页码
pageSize int 每页条数

ImportResult 字段

字段 类型 说明
successCount int 成功行数
failCount int 失败行数
errorDetails List<String> 错误详情列表

处理器注册机制

ImportExportHandlerRegistry 在 Spring 容器初始化时自动扫描所有 ImportDataHandlerExportDataHandler 实现,按 getBusinessType() 返回值注册映射。业务模块只需实现接口并声明为 Spring Bean 即可,无需手动注册。

使用示例

实现导入导出处理器

@Component
public class UserImportExportHandler implements ImportDataHandler, ExportDataHandler {

    private final UserRepository userRepository;

    @Override
    public String getBusinessType() {
        return "user";
    }

    @Override
    public ImportResult importData(List<Map<String, Object>> rows, ImportContext context) {
        int success = 0, fail = 0;
        List<String> errors = new ArrayList<>();
        for (Map<String, Object> row : rows) {
            try {
                User user = new User();
                user.setUsername((String) row.get("username"));
                user.setNickname((String) row.get("nickname"));
                user.setMobile((String) row.get("mobile"));
                userRepository.save(user);
                success++;
            } catch (Exception e) {
                fail++;
                errors.add("行 " + (success + fail + 1) + ": " + e.getMessage());
            }
        }
        return ImportResult.builder()
                .successCount(success).failCount(fail).errorDetails(errors)
                .build();
    }

    @Override
    public List<Map<String, Object>> queryExportData(ExportContext context) {
        List<User> users = userRepository.lambdaQuery().list();
        return users.stream().map(u -> {
            Map<String, Object> map = new LinkedHashMap<>();
            map.put("username", u.getUsername());
            map.put("nickname", u.getNickname());
            map.put("mobile", u.getMobile());
            return map;
        }).collect(Collectors.toList());
    }
}

创建导入导出模板

await post('/v1/ie/template/create', {
  templateName: '用户导入模板',
  businessType: 'user',
  templateType: 3,   // BOTH - 双向
  sheetName: '用户数据',
  columnsConfig: [
    { fieldName: 'username', fieldLabel: '用户名', columnType: 'STRING', required: true, maxLength: 50, orderNum: 1 },
    { fieldName: 'nickname', fieldLabel: '昵称', columnType: 'STRING', maxLength: 30, orderNum: 2 },
    { fieldName: 'mobile',   fieldLabel: '手机号', columnType: 'STRING', required: true, validateRegex: '^1[3-9]\\d{9}$', orderNum: 3 }
  ]
})

执行导入和导出

// 导入预览
const formData = new FormData()
formData.append('templateId', templateId)
formData.append('file', excelFile)
const { data: preview } = await post('/v1/ie/task/importPreview', formData)
// preview.rows = 前10行数据, preview.errors = 校验错误

// 确认导入
const { data: taskId } = await post('/v1/ie/task/import', formData)

// 触发异步导出
const { data: exportTaskId } = await post('/v1/ie/task/export', {
  templateId: templateId,
  params: '{}',
  selectedColumns: ['username', 'nickname', 'mobile']
})

// 轮询导出进度
const { data: task } = await get(`/v1/ie/task/get/${exportTaskId}`)
// task.progress = 0~100, task.status = 0~4

注意事项

  • 导出任务为异步执行,通过 ieExportExecutor 线程池处理,需通过任务详情接口轮询进度。
  • 导入预览只解析前 10 行数据并执行校验,不执行实际写入。
  • 模板的 columnsConfig 为 JSON 数组,定义了列的字段映射、类型、校验规则和排序。
  • 处理器的 getBusinessType() 返回值必须与模板的 businessType 字段一致。
  • 导出完成后,Excel 文件会自动上传到文件模块(scaffold-file),任务记录中保存文件 URL。
  • 单次导入/导出最大行数默认 10000,可通过 scaffold.ie.max-rows 调整。
  • 模块启用条件:scaffold.ie.enabled=true(默认启用)。
  • 状态枚举
    • status 字段(模板)遵循布尔语义:1 = 启用,0 = 禁用
    • TaskStatusEnum 为多态状态机,不遵循布尔语义规范