scaffold-import-export 导入导出模块
模块概述
scaffold-import-export(模块名 scaffold-ie)是基于 EasyExcel 的通用导入导出框架。采用 SPI 架构,业务模块只需实现 ImportDataHandler 和 ExportDataHandler 接口,即可通过统一的模板配置实现 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 容器初始化时自动扫描所有 ImportDataHandler 和 ExportDataHandler 实现,按 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 为多态状态机,不遵循布尔语义规范