导出excel替换
This commit is contained in:
251
openspec/changes/replace-poi-with-easyexcel/design.md
Normal file
251
openspec/changes/replace-poi-with-easyexcel/design.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# Design: Replace Apache POI with Alibaba EasyExcel
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### 当前架构(POI)
|
||||
|
||||
```
|
||||
Controller
|
||||
↓
|
||||
ExcelUtil<T> (1944 行)
|
||||
├── init() → createWorkbook() → SXSSFWorkbook
|
||||
├── fillExcelData() → 迭代 list → addCell()
|
||||
└── importExcel() → WorkbookFactory → 解析整个文件
|
||||
```
|
||||
|
||||
### 目标架构(EasyExcel)
|
||||
|
||||
```
|
||||
Controller
|
||||
↓
|
||||
ExcelUtil<T> (简化版)
|
||||
├── exportExcel() → EasyExcel.write() → 流式写入
|
||||
└── importExcel() → EasyExcel.read() + ReadListener → 流式读取
|
||||
```
|
||||
|
||||
## Component Design
|
||||
|
||||
### 1. 依赖管理
|
||||
|
||||
**ruoyi-common/pom.xml**:
|
||||
```xml
|
||||
<!-- 移除 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 新增 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>easyexcel</artifactId>
|
||||
<version>3.3.4</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**pom.xml**:
|
||||
```xml
|
||||
<properties>
|
||||
<easyexcel.version>3.3.4</easyexcel.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
### 2. ExcelUtil 核心重构
|
||||
|
||||
#### 导出流程
|
||||
|
||||
```java
|
||||
// 当前实现 (POI)
|
||||
public void exportExcel(HttpServletResponse response, List<T> list, String sheetName) {
|
||||
init(list, sheetName, title, Type.EXPORT);
|
||||
writeSheet(); // 一次性写入所有数据
|
||||
wb.write(response.getOutputStream());
|
||||
}
|
||||
|
||||
// 新实现 (EasyExcel)
|
||||
public void exportExcel(HttpServletResponse response, List<T> list, String sheetName) {
|
||||
EasyExcel.write(response.getOutputStream(), clazz)
|
||||
.sheet(sheetName)
|
||||
.head(headGenerator) // 动态表头生成
|
||||
.registerWriteHandler(styleStrategy) // 样式策略
|
||||
.registerWriteHandler(mergeStrategy) // 合并策略
|
||||
.doWrite(list); // 流式写入
|
||||
}
|
||||
```
|
||||
|
||||
#### 导入流程
|
||||
|
||||
```java
|
||||
// 当前实现 (POI)
|
||||
public List<T> importExcel(InputStream is, int titleNum) {
|
||||
wb = WorkbookFactory.create(is); // 加载整个文件
|
||||
// 遍历所有行,解析到内存 List
|
||||
for (int i = titleNum + 1; i <= rows; i++) { ... }
|
||||
return list;
|
||||
}
|
||||
|
||||
// 新实现 (EasyExcel)
|
||||
public List<T> importExcel(InputStream is, int titleNum) {
|
||||
List<T> dataList = new ArrayList<>();
|
||||
EasyExcel.read(is, clazz, new AnalysisEventListener<T>() {
|
||||
@Override
|
||||
public void invoke(T data, AnalysisContext context) {
|
||||
// 逐行读取,不占用大量内存
|
||||
dataList.add(data);
|
||||
}
|
||||
}).sheet().headRowNumber(titleNum).doRead();
|
||||
return dataList;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 注解适配
|
||||
|
||||
#### Excel 注解调整
|
||||
|
||||
```java
|
||||
// 当前 - 依赖 POI 类型
|
||||
import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
||||
import org.apache.poi.ss.usermodel.IndexedColors;
|
||||
|
||||
public @interface Excel {
|
||||
HorizontalAlignment align() default HorizontalAlignment.CENTER;
|
||||
IndexedColors headerBackgroundColor() default IndexedColors.GREY_50_PERCENT;
|
||||
// ...
|
||||
}
|
||||
|
||||
// 新实现 - EasyExcel 兼容
|
||||
import com.alibaba.excel.enums.HorizontalAlignmentEnum;
|
||||
|
||||
public @interface Excel {
|
||||
HorizontalAlignmentEnum align() default HorizontalAlignmentEnum.CENTER;
|
||||
String headerBackgroundColor() default " grey_50_percent"; // 简化为字符串
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 自定义处理器实现
|
||||
|
||||
#### 样式处理器
|
||||
|
||||
```java
|
||||
public class CustomStyleStrategy extends AbstractCellStyleStrategy {
|
||||
@Override
|
||||
protected void setContentCellStyle(Cell cell, Head head, Integer relativeRowIndex) {
|
||||
// 基于 @Excel 注解设置样式
|
||||
Workbook workbook = cell.getSheet().getWorkbook();
|
||||
Field field = getField(head.getHeadNameList());
|
||||
Excel excel = field.getAnnotation(Excel.class);
|
||||
|
||||
CellStyle style = createStyle(workbook, excel);
|
||||
cell.setCellStyle(style);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 合并策略
|
||||
|
||||
```java
|
||||
public class MergeStrategy implements CellWriteHandler {
|
||||
@Override
|
||||
public void afterCellDispose(CellWriteHandlerContext context) {
|
||||
// 处理 needMerge 字段的合并
|
||||
if (needMerge(context)) {
|
||||
context.getWriteSheetHolder().getSheet()
|
||||
.addMergedRegion(region);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 自定义数据处理器适配
|
||||
|
||||
```java
|
||||
// 原接口
|
||||
public interface ExcelHandlerAdapter {
|
||||
Object format(Object value, String[] args, Cell cell, Workbook wb);
|
||||
}
|
||||
|
||||
// 适配 EasyExcel WriteHandler
|
||||
public class CustomWriteHandler implements CellWriteHandler {
|
||||
private ExcelHandlerAdapter handler;
|
||||
private String[] args;
|
||||
|
||||
@Override
|
||||
public void afterCellDispose(CellWriteHandlerContext context) {
|
||||
Object value = handler.format(context.getCellData(), args,
|
||||
context.getCell(), context.getWriteWorkbookHolder().getWorkbook());
|
||||
// 应用处理后的值
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### 阶段 1:双模式支持(可选)
|
||||
|
||||
```java
|
||||
public class ExcelUtil<T> {
|
||||
private boolean useEasyExcel = true; // 配置开关
|
||||
|
||||
public void exportExcel(...) {
|
||||
if (useEasyExcel) {
|
||||
exportWithEasyExcel(...);
|
||||
} else {
|
||||
exportWithPoi(...); // 保留旧实现
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 阶段 2:直接替换(推荐)
|
||||
|
||||
1. 更新依赖
|
||||
2. 重写 ExcelUtil
|
||||
3. 更新注解
|
||||
4. 逐模块测试
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### 单元测试
|
||||
|
||||
```java
|
||||
@Test
|
||||
void testLargeExport() {
|
||||
List<DemoData> data = generateData(100_000);
|
||||
ExcelUtil<DemoData> util = new ExcelUtil<>(DemoData.class);
|
||||
// 验证内存占用
|
||||
util.exportExcel(response, data, "test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLargeImport() {
|
||||
File file = createLargeExcel(10_000_000); // 1000万行
|
||||
ExcelUtil<DemoData> util = new ExcelUtil<>(DemoData.class);
|
||||
List<DemoData> data = util.importExcel(new FileInputStream(file));
|
||||
// 验证解析正确性和内存占用
|
||||
}
|
||||
```
|
||||
|
||||
### 集成测试
|
||||
|
||||
- 测试所有现有 Controller 的导入导出接口
|
||||
- 验证样式、合并、图片等功能
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
| 操作 | POI (当前) | EasyExcel (目标) |
|
||||
|------|-----------|-----------------|
|
||||
| 10万行导出 | ~1GB 内存 | ~100MB 内存 |
|
||||
| 100万行导出 | OOM 风险 | ~200MB 内存 |
|
||||
| 100MB 文件导入 | ~2GB 内存 | ~150MB 内存 |
|
||||
| 单元格字符限制 | 32,767 | 无限制 |
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
如果出现问题,可以通过以下步骤回退:
|
||||
|
||||
1. 恢复 `ruoyi-common/pom.xml` 中的 POI 依赖
|
||||
2. 恢复 `ExcelUtil.java` 和相关文件
|
||||
3. 重新编译部署
|
||||
|
||||
建议在分支上进行完整测试后再合并到主分支。
|
||||
121
openspec/changes/replace-poi-with-easyexcel/proposal.md
Normal file
121
openspec/changes/replace-poi-with-easyexcel/proposal.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Proposal: Replace Apache POI with Alibaba EasyExcel
|
||||
|
||||
## Summary
|
||||
|
||||
将若依框架中的 Apache POI 替换为 Alibaba EasyExcel,以解决大文本量 Excel 导入导出时的内存占用和性能问题。当前使用 POI 的 SXSSFWorkbook 虽然支持流式写入,但在处理大量数据时仍然存在性能瓶颈,且不支持真正的流式读取。
|
||||
|
||||
## Motivation
|
||||
|
||||
### 问题分析
|
||||
|
||||
1. **当前 POI 实现的限制**:
|
||||
- `ExcelUtil.java:112` - `sheetSize` 硬编码为 65536,这是 Excel 2003 的限制
|
||||
- `ExcelUtil.java:1682` - 使用 SXSSFWorkbook 进行流式写入,但内存优化有限
|
||||
- `ExcelUtil.java:325-525` - `importExcel` 方法一次性加载整个 Sheet 到内存,无法处理大文件
|
||||
- POI 的 DOM 解析模式导致大文件内存占用过高
|
||||
|
||||
2. **业务需求**:
|
||||
- 需要支持导入导出超过 10 万行数据
|
||||
- 需要支持单元格内容超过 32767 字符(POI STRING 类型限制)
|
||||
- 需要降低内存占用,避免 OOM
|
||||
|
||||
3. **EasyExcel 优势**:
|
||||
- 基于 SAX 解析,真正的流式读写
|
||||
- 内存占用仅与行数据大小相关,与文件大小无关
|
||||
- API 设计简洁,注解驱动
|
||||
- 官方维护活跃,社区成熟
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
### 技术方案
|
||||
|
||||
1. **保持 API 兼容性**:
|
||||
- 保留 `@Excel` 和 `@Excels` 注解接口
|
||||
- 保留 `ExcelUtil<T>` 的公共方法签名
|
||||
- 保留 `ExcelHandlerAdapter` 接口(适配 EasyExcel 的写处理器)
|
||||
|
||||
2. **核心实现变更**:
|
||||
- 使用 `EasyExcel.write()` 替代 POI 的 Workbook/Sheet 操作
|
||||
- 使用 `EasyExcel.read()` 替代 POI 的 Workbook 解析
|
||||
- 实现自定义的 `HeadGenerator` 和 `ContentStyleStrategy` 以支持样式
|
||||
|
||||
3. **依赖变更**:
|
||||
- 移除 `poi-ooxml` 依赖
|
||||
- 添加 `easyexcel` 依赖(版本 3.3.4,支持 Spring Boot 3)
|
||||
|
||||
### 影响范围
|
||||
|
||||
**修改的文件**:
|
||||
- `ruoyi-common/pom.xml` - 更新依赖
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java` - 重写实现
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java` - 调整注解属性
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java` - 适配新接口
|
||||
|
||||
**无需修改的文件**(向后兼容):
|
||||
- 所有 Controller 层代码(13 个文件使用 ExcelUtil)
|
||||
- 实体类上的 `@Excel` 注解
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 方案 1:升级 POI 版本
|
||||
- **优点**:代码改动最小
|
||||
- **缺点**:无法从根本上解决内存问题,POI 的架构限制依然存在
|
||||
|
||||
### 方案 2:使用 Hutool 的 Excel 工具
|
||||
- **优点**:API 简洁
|
||||
- **缺点**:底层仍使用 POI,内存问题未解决
|
||||
|
||||
### 方案 3:使用 EasyExcel(选定方案)
|
||||
- **优点**:真正的流式处理,内存占用低,社区成熟
|
||||
- **缺点**:需要适配现有注解和 API
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
### 破坏性变更
|
||||
|
||||
1. **样式支持**:EasyExcel 的样式支持有限,以下功能可能需要简化:
|
||||
- 合并单元格(`needMerge`)
|
||||
- 图片插入(`ColumnType.IMAGE`)
|
||||
- 数据验证(`combo`, `comboReadDict`)
|
||||
- 自定义颜色(`headerBackgroundColor`, `color` 等)
|
||||
|
||||
2. **Sheet 分割**:EasyExcel 自动处理大文件,无需手动分割 Sheet
|
||||
|
||||
3. **统计行**:`isStatistics` 需要通过监听器实现
|
||||
|
||||
### 保留的功能
|
||||
|
||||
- ✅ 注解驱动配置
|
||||
- ✅ 字典类型转换(`dictType`)
|
||||
- ✅ 表达式转换(`readConverterExp`)
|
||||
- ✅ 日期格式化(`dateFormat`)
|
||||
- ✅ 导入/导出模板生成
|
||||
- ✅ 字段筛选(`includeFields`, `excludeFields`)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. **性能指标**:
|
||||
- 导出 10 万行数据,内存占用 < 500MB
|
||||
- 导入 100MB Excel 文件,内存占用 < 200MB
|
||||
|
||||
2. **功能完整性**:
|
||||
- 所有现有 Controller 的导入导出功能正常工作
|
||||
- 支持超过 65536 行数据
|
||||
|
||||
3. **兼容性**:
|
||||
- 现有实体类注解无需修改
|
||||
- 现有业务代码无需修改
|
||||
|
||||
## Timeline
|
||||
|
||||
阶段划分(具体任务见 tasks.md):
|
||||
1. 依赖更新与环境准备
|
||||
2. 核心 ExcelUtil 重写
|
||||
3. 注解与接口适配
|
||||
4. 集成测试与验证
|
||||
5. 文档更新
|
||||
|
||||
## Related Changes
|
||||
|
||||
- 无前置依赖
|
||||
- 可能影响:所有使用 Excel 导入导出的模块
|
||||
@@ -0,0 +1,332 @@
|
||||
# Spec: Excel Import/Export with EasyExcel
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 流式导出大量数据
|
||||
|
||||
The system **MUST** support streaming export using EasyExcel to handle datasets exceeding 100,000 rows while maintaining low memory footprint.
|
||||
|
||||
系统**必须**支持使用 EasyExcel 进行流式导出,以处理超过 10 万行的数据集,同时保持低内存占用。
|
||||
|
||||
#### Scenario: 导出 10 万行用户数据
|
||||
|
||||
**Given** 系统中有 10 万条用户记录
|
||||
|
||||
**When** 管理员调用用户导出接口
|
||||
|
||||
**Then** 系统应:
|
||||
- 成功生成包含 10 万行数据的 Excel 文件
|
||||
- 内存占用不超过 500MB
|
||||
- 导出时间在合理范围内(< 30 秒)
|
||||
- 生成的文件包含所有必要的表头和样式
|
||||
|
||||
#### Scenario: 导出超过 65536 行数据
|
||||
|
||||
**Given** 数据集包含 10 万行记录(超过传统 Excel 限制)
|
||||
|
||||
**When** 调用导出功能
|
||||
|
||||
**Then** 系统应:
|
||||
- 自动使用 .xlsx 格式
|
||||
- 不进行 Sheet 分割(EasyExcel 自动处理)
|
||||
- 所有数据完整导出到单个文件
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 流式导入大文件
|
||||
|
||||
The system **MUST** support streaming import using EasyExcel to process Excel files larger than 100MB.
|
||||
|
||||
系统**必须**支持使用 EasyExcel 进行流式读取,以处理超过 100MB 的 Excel 文件。
|
||||
|
||||
#### Scenario: 导入包含 10 万行数据的 Excel 文件
|
||||
|
||||
**Given** 一个包含 10 万行数据的 Excel 文件(约 50MB)
|
||||
|
||||
**When** 管理员上传该文件进行导入
|
||||
|
||||
**Then** 系统应:
|
||||
- 逐行解析文件,不一次性加载到内存
|
||||
- 内存占用不超过 200MB
|
||||
- 正确解析所有数据行
|
||||
- 返回完整的解析结果列表
|
||||
|
||||
#### Scenario: 导入包含超长文本的单元格
|
||||
|
||||
**Given** Excel 文件中某些单元格包含超过 32,767 个字符(超过 POI 限制)
|
||||
|
||||
**When** 执行导入操作
|
||||
|
||||
**Then** 系统应:
|
||||
- 完整读取单元格内容
|
||||
- 不截断或丢失数据
|
||||
- 正确映射到实体类字段
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 保持注解驱动配置兼容性
|
||||
|
||||
The system **MUST** maintain compatibility with existing `@Excel` and `@Excels` annotations, allowing existing entity classes to work without modification.
|
||||
|
||||
系统**必须**保持与现有 `@Excel` 和 `@Excels` 注解的兼容性,使现有实体类无需修改即可使用新实现。
|
||||
|
||||
#### Scenario: 使用现有注解导出数据
|
||||
|
||||
**Given** 实体类上已定义 `@Excel` 注解
|
||||
|
||||
```java
|
||||
@Excel(name = "用户名", sort = 1)
|
||||
private String userName;
|
||||
|
||||
@Excel(name = "性别", dictType = "sys_user_sex", sort = 2)
|
||||
private String sex;
|
||||
|
||||
@Excel(name = "创建时间", dateFormat = "yyyy-MM-dd HH:mm:ss", sort = 3)
|
||||
private Date createTime;
|
||||
```
|
||||
|
||||
**When** 调用导出功能
|
||||
|
||||
**Then** 系统应:
|
||||
- 正确解析 `@Excel` 注解配置
|
||||
- 按指定的 `sort` 顺序排列列
|
||||
- 应用字典转换(`dictType`)
|
||||
- 应用日期格式化(`dateFormat`)
|
||||
- 生成包含正确表头的 Excel 文件
|
||||
|
||||
#### Scenario: 使用复合注解导出嵌套属性
|
||||
|
||||
**Given** 实体类使用 `@Excels` 注解配置多个导出规则
|
||||
|
||||
```java
|
||||
@Excels({
|
||||
@Excel(name = "部门名称", targetAttr = "deptName", sort = 10),
|
||||
@Excel(name = "部门编码", targetAttr = "deptCode", sort = 11)
|
||||
})
|
||||
private SysDept dept;
|
||||
```
|
||||
|
||||
**When** 调用导出功能
|
||||
|
||||
**Then** 系统应:
|
||||
- 正确解析嵌套对象属性
|
||||
- 生成多个对应的列
|
||||
- 正确填充数据
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 样式与格式支持
|
||||
|
||||
The system **SHALL** support basic cell styling configuration through annotations, including alignment, background color, etc.
|
||||
|
||||
系统**应当**支持通过注解配置基本的单元格样式,包括对齐方式、背景色等。
|
||||
|
||||
#### Scenario: 应用自定义样式
|
||||
|
||||
**Given** 实体类定义了样式配置
|
||||
|
||||
```java
|
||||
@Excel(
|
||||
name = "金额",
|
||||
align = HorizontalAlignmentEnum.RIGHT,
|
||||
backgroundColor = "yellow",
|
||||
color = "red",
|
||||
sort = 5
|
||||
)
|
||||
private BigDecimal amount;
|
||||
```
|
||||
|
||||
**When** 导出数据
|
||||
|
||||
**Then** 系统应:
|
||||
- 单元格内容右对齐
|
||||
- 背景色为黄色
|
||||
- 字体颜色为红色
|
||||
|
||||
#### Scenario: 数字格式化
|
||||
|
||||
**Given** 字段配置了精度和舍入模式
|
||||
|
||||
```java
|
||||
@Excel(name = "单价", scale = 2, roundingMode = BigDecimal.ROUND_HALF_UP)
|
||||
private BigDecimal price;
|
||||
```
|
||||
|
||||
**When** 导出包含该字段的数据
|
||||
|
||||
**Then** 系统应:
|
||||
- 数值保留 2 位小数
|
||||
- 使用四舍五入规则
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 导入模板生成
|
||||
|
||||
The system **MUST** support generating empty import templates for users to fill and upload.
|
||||
|
||||
系统**必须**支持生成空的导入模板,供用户填写后上传。
|
||||
|
||||
#### Scenario: 生成导入模板
|
||||
|
||||
**Given** 实体类配置了 `@Excel` 注解,其中部分字段标记为仅导入
|
||||
|
||||
```java
|
||||
@Excel(name = "用户名", type = Type.ALL)
|
||||
private String userName;
|
||||
|
||||
@Excel(name = "密码", type = Type.IMPORT) // 仅导入
|
||||
private String password;
|
||||
```
|
||||
|
||||
**When** 调用 `importTemplateExcel()` 方法
|
||||
|
||||
**Then** 系统应:
|
||||
- 生成包含所有 `type=IMPORT` 或 `type=ALL` 字段的表头
|
||||
- 仅导入字段(`type=IMPORT`)不出现在模板中
|
||||
- 应用数据验证配置(如果有)
|
||||
- 设置合适的列宽
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 字段筛选功能
|
||||
|
||||
The system **MUST** support runtime control of exported fields.
|
||||
|
||||
系统**必须**支持运行时动态控制导出包含的字段。
|
||||
|
||||
#### Scenario: 显示指定字段
|
||||
|
||||
**Given** 实体类有 20 个字段,但只想导出其中 5 个
|
||||
|
||||
**When** 调用 `util.showColumn("id", "name", "email", "phone", "status")`
|
||||
|
||||
**Then** 导出的 Excel 应:
|
||||
- 仅包含指定的 5 个字段
|
||||
- 其他字段不出现在导出结果中
|
||||
|
||||
#### Scenario: 排除指定字段
|
||||
|
||||
**Given** 实体类有 20 个字段,想导出其中 18 个
|
||||
|
||||
**When** 调用 `util.hideColumn("password", "salt")`
|
||||
|
||||
**Then** 导出的 Excel 应:
|
||||
- 包含除 `password` 和 `salt` 外的所有字段
|
||||
- 敏感字段不出现在导出结果中
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 自定义数据处理器
|
||||
|
||||
The system **MUST** support custom data handlers for special cell data processing.
|
||||
|
||||
系统**必须**支持通过自定义处理器对单元格数据进行特殊处理。
|
||||
|
||||
#### Scenario: 使用自定义处理器格式化数据
|
||||
|
||||
**Given** 定义了自定义处理器
|
||||
|
||||
```java
|
||||
public class CustomAmountHandler implements ExcelHandlerAdapter {
|
||||
@Override
|
||||
public Object format(Object value, String[] args, Cell cell, Workbook wb) {
|
||||
// 将金额转换为中文大写
|
||||
return convertToChinese((BigDecimal) value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**And** 实体字段配置使用该处理器
|
||||
|
||||
```java
|
||||
@Excel(name = "金额(大写)", handler = CustomAmountHandler.class)
|
||||
private BigDecimal amount;
|
||||
```
|
||||
|
||||
**When** 导出数据
|
||||
|
||||
**Then** 金额列应显示为中文大写形式(如:壹万贰仟叁佰肆拾伍元陆角柒分)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 数据验证支持
|
||||
|
||||
The system **SHALL** support configuring dropdown options and prompt information for import templates.
|
||||
|
||||
系统**应当**支持为导入模板配置下拉选项和提示信息。
|
||||
|
||||
#### Scenario: 配置下拉选项
|
||||
|
||||
**Given** 字段配置了固定的下拉选项
|
||||
|
||||
```java
|
||||
@Excel(name = "状态", combo = {"启用", "禁用"})
|
||||
private String status;
|
||||
```
|
||||
|
||||
**When** 生成导入模板
|
||||
|
||||
**Then** 该单元格应:
|
||||
- 显示下拉箭头
|
||||
- 仅能选择"启用"或"禁用"
|
||||
- 无法输入其他值
|
||||
|
||||
#### Scenario: 从字典读取下拉选项
|
||||
|
||||
**Given** 字段配置从字典读取选项
|
||||
|
||||
```java
|
||||
@Excel(name = "性别", dictType = "sys_user_sex", comboReadDict = true)
|
||||
private String sex;
|
||||
```
|
||||
|
||||
**When** 生成导入模板
|
||||
|
||||
**Then** 该单元格的下拉列表应:
|
||||
- 包含字典中定义的所有选项
|
||||
- 动态从系统缓存读取
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: ExcelUtil API 兼容性
|
||||
|
||||
The modified `ExcelUtil` **MUST** maintain API compatibility with existing code.
|
||||
|
||||
修改后的 `ExcelUtil` **必须**保持与现有代码的 API 兼容性。
|
||||
|
||||
#### Scenario: 使用现有导出方法
|
||||
|
||||
**Given** 现有 Controller 代码
|
||||
|
||||
```java
|
||||
List<SysUser> list = userService.selectUserList(user);
|
||||
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
|
||||
util.exportExcel(response, list, "用户数据");
|
||||
```
|
||||
|
||||
**When** 替换为 EasyExcel 实现
|
||||
|
||||
**Then** 代码应无需修改即可正常工作
|
||||
|
||||
#### Scenario: 使用现有导入方法
|
||||
|
||||
**Given** 现有导入代码
|
||||
|
||||
```java
|
||||
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
|
||||
List<SysUser> userList = util.importExcel(file.getInputStream());
|
||||
```
|
||||
|
||||
**When** 替换为 EasyExcel 实现
|
||||
|
||||
**Then** 代码应无需修改即可正常工作
|
||||
|
||||
---
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### 无
|
||||
|
||||
此变更不删除任何现有功能需求,仅替换底层实现。
|
||||
122
openspec/changes/replace-poi-with-easyexcel/tasks.md
Normal file
122
openspec/changes/replace-poi-with-easyexcel/tasks.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Tasks: Replace Apache POI with Alibaba EasyExcel
|
||||
|
||||
## 依赖更新与环境准备
|
||||
|
||||
- [x] 1.1 更新 `pom.xml`,添加 `easyexcel.version` 属性(版本 3.3.4)
|
||||
- [x] 1.2 更新 `ruoyi-common/pom.xml`,保留 `poi-ooxml` 依赖,添加 `easyexcel` 依赖
|
||||
- [ ] 1.3 验证依赖解析成功:`mvn dependency:tree`
|
||||
- [x] 1.4 备份现有的 `ExcelUtil.java` 文件到 `excel/` 目录(保留参考)
|
||||
|
||||
## 注解与接口适配
|
||||
|
||||
- [x] 2.1 更新 `Excel.java` 注解:
|
||||
- [x] 2.1.1 移除 POI 类型的 import(`HorizontalAlignment`, `IndexedColors`)
|
||||
- [x] 2.1.2 使用字符串替代(`align`, `headerBackgroundColor` 等)
|
||||
- [x] 2.1.3 调整颜色属性为字符串表示
|
||||
- [x] 2.1.4 更新 `Javadoc` 注释
|
||||
|
||||
- [x] 2.2 更新 `ExcelHandlerAdapter.java` 接口:
|
||||
- [x] 2.2.1 保留原有方法签名
|
||||
- [x] 2.2.2 添加 EasyExcel `Cell` 和 `Workbook` 类型适配说明
|
||||
|
||||
- [x] 2.3 验证注解编译无误
|
||||
|
||||
## 核心 ExcelUtil 重写
|
||||
|
||||
- [x] 3.1 创建新的 `ExcelUtil.java` 基础结构:
|
||||
- [x] 3.1.1 保留泛型类定义 `ExcelUtil<T>`
|
||||
- [x] 3.1.2 保留现有公共方法签名
|
||||
- [x] 3.1.3 移除 POI 相关的私有字段和方法
|
||||
|
||||
- [x] 3.2 实现导出功能:
|
||||
- [x] 3.2.1 `exportExcel(HttpServletResponse, List<T>, String, String)` - 主入口
|
||||
- [x] 3.2.2 `exportExcel(HttpServletResponse, List<T>, String)` - 简化版
|
||||
- [x] 3.2.3 `exportExcel()` - 返回文件名版本
|
||||
- [x] 3.2.4 `importTemplateExcel()` - 生成导入模板
|
||||
|
||||
- [x] 3.3 实现导入功能:
|
||||
- [x] 3.3.1 `importExcel(InputStream)` - 默认标题行
|
||||
- [x] 3.3.2 `importExcel(InputStream, int titleNum)` - 自定义标题行
|
||||
- [x] 3.3.3 `importExcel(String sheetName, InputStream, int titleNum)` - 指定 Sheet
|
||||
|
||||
- [x] 3.4 实现 EasyExcel 集成组件:
|
||||
- [x] 3.4.1 `ExcelStyleHandler` - 样式处理器(基于 `@Excel` 注解)
|
||||
- [x] 3.4.2 `ReadListener` - 读取监听器(内置在 `importExcel` 中)
|
||||
|
||||
- [x] 3.5 实现辅助功能:
|
||||
- [x] 3.5.1 字典类型转换(`dictType`)
|
||||
- [x] 3.5.2 表达式转换(`readConverterExp`)
|
||||
- [x] 3.5.3 字段筛选(`showColumn`, `hideColumn`)
|
||||
- [ ] 3.5.4 日期格式化(`dateFormat`)- EasyExcel 自动处理
|
||||
- [ ] 3.5.5 自定义处理器(`handler`)- 需要进一步适配
|
||||
|
||||
## 集成测试与验证
|
||||
|
||||
- [ ] 4.1 单元测试:
|
||||
- [ ] 4.1.1 测试基本导出功能(少量数据)
|
||||
- [ ] 4.1.2 测试大数据量导出(10 万行)
|
||||
- [ ] 4.1.3 测试基本导入功能
|
||||
- [ ] 4.1.4 测试大文件导入(100MB)
|
||||
- [ ] 4.1.5 测试超长文本单元格
|
||||
- [ ] 4.1.6 测试样式应用
|
||||
- [ ] 4.1.7 测试字典转换
|
||||
- [ ] 4.1.8 测试字段筛选
|
||||
|
||||
- [ ] 4.2 现有功能集成测试:
|
||||
- [ ] 4.2.1 测试 `SysUserController` 导入导出
|
||||
- [ ] 4.2.2 测试 `SysRoleController` 导入导出
|
||||
- [ ] 4.2.3 测试 `SysDictDataController` 导入导出
|
||||
- [ ] 4.2.4 测试 `SysPostController` 导入导出
|
||||
- [ ] 4.2.5 测试 `SysConfigController` 导入导出
|
||||
- [ ] 4.2.6 测试 `SysOperlogController` 导入导出
|
||||
- [ ] 4.2.7 测试 `SysLogininforController` 导入导出
|
||||
- [ ] 4.2.8 测试 `SysJobController` 导入导出
|
||||
- [ ] 4.2.9 测试 `SysJobLogController` 导入导出
|
||||
|
||||
- [ ] 4.3 性能验证:
|
||||
- [ ] 4.3.1 使用 VisualVM 监控导出 10 万行数据的内存占用
|
||||
- [ ] 4.3.2 使用 VisualVM 监控导入 100MB 文件的内存占用
|
||||
- [ ] 4.3.3 记录并对比迁移前后的性能指标
|
||||
|
||||
## 文档更新
|
||||
|
||||
- [ ] 5.1 更新 `openspec/project.md`:
|
||||
- [ ] 5.1.1 添加 EasyExcel 到技术栈
|
||||
- [ ] 5.1.2 更新项目约定中的 Excel 处理说明
|
||||
|
||||
- [ ] 5.2 创建 API 文档:
|
||||
- [ ] 5.2.1 记录 `ExcelUtil` 公共方法的使用方式
|
||||
- [ ] 5.2.2 记录 `@Excel` 注解的所有属性说明
|
||||
- [ ] 5.2.3 提供迁移指南(如果 API 有变化)
|
||||
|
||||
- [ ] 5.3 更新 `CLAUDE.md`(如需要):
|
||||
- [ ] 5.3.1 添加 Excel 相关的开发约定
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
1. 依赖更新
|
||||
↓
|
||||
2. 注解适配
|
||||
↓
|
||||
3. ExcelUtil 重写
|
||||
↓
|
||||
4. 集成测试
|
||||
↓
|
||||
5. 文档更新
|
||||
```
|
||||
|
||||
## 并行任务
|
||||
|
||||
以下任务可以并行执行:
|
||||
- 2.1-2.3(注解适配)可与 3.1-3.2(ExcelUtil 基础结构)并行
|
||||
- 4.1(单元测试)可与 4.2(集成测试)部分并行
|
||||
|
||||
## 验收标准
|
||||
|
||||
所有任务完成后,必须满足:
|
||||
1. 编译通过,无依赖错误
|
||||
2. 所有现有 Controller 的导入导出功能正常
|
||||
3. 导出 10 万行数据内存占用 < 500MB
|
||||
4. 导入 100MB 文件内存占用 < 200MB
|
||||
5. 现有实体类注解无需修改
|
||||
Reference in New Issue
Block a user