|
| 1 | +# Issue #666 问题分析与解决方案 |
| 2 | + |
| 3 | +## 问题描述 |
| 4 | + |
| 5 | +在使用动态表头写入时,C2 和 D2 单元格被错误地自动合并。根据 issue #666 的描述: |
| 6 | + |
| 7 | +### 测试用例 |
| 8 | +```java |
| 9 | +List<List<String>> multiHeader = new ArrayList<>(); |
| 10 | +multiHeader.add(new ArrayList<>(Arrays.asList("head10"))); |
| 11 | +multiHeader.add(new ArrayList<>(Arrays.asList("head20", "head21"))); |
| 12 | +multiHeader.add(new ArrayList<>(Arrays.asList("head30", "head31"))); |
| 13 | +multiHeader.add(new ArrayList<>(Arrays.asList("head40", "head31"))); |
| 14 | +multiHeader.add(new ArrayList<>(Arrays.asList("head40", "head41"))); |
| 15 | +``` |
| 16 | + |
| 17 | +### 表头结构(按列视角) |
| 18 | +- **列0**: `["head10", "head20", "head30", "head40", "head40"]` |
| 19 | +- **列1**: `[null, "head21", "head31", "head31", "head41"]` |
| 20 | + |
| 21 | +### 问题现象 |
| 22 | +- **当前行为**: C2 和 D2 被自动合并 |
| 23 | +- **预期行为**: C2 和 D2 不应该合并 |
| 24 | + |
| 25 | +## 根本原因分析 |
| 26 | + |
| 27 | +### 当前合并算法的问题 |
| 28 | + |
| 29 | +在 `ExcelWriteHeadProperty.headCellRangeList()` 方法中(第113-160行),合并算法存在以下问题: |
| 30 | + |
| 31 | +1. **缺乏矩形区域验证**: |
| 32 | + - 算法会先水平查找(向右扩展),然后垂直查找(向下扩展) |
| 33 | + - 但没有验证整个矩形区域内的所有单元格是否都具有相同的名称 |
| 34 | + - 这可能导致不完整的矩形区域被错误地合并 |
| 35 | + |
| 36 | +2. **垂直合并逻辑缺陷**: |
| 37 | + - 当处理第3行的 "head31"(列1,行2)时: |
| 38 | + - 算法先水平查找,发现没有相邻的相同名称 |
| 39 | + - 然后垂直向下查找,发现第4行(列1,行3)也有 "head31" |
| 40 | + - 算法会创建一个合并区域:行2-3,列1-1 |
| 41 | + - **问题**:虽然两行的 "head31" 名称相同,但它们上方单元格的上下文不同(第3行上方是 "head30",第4行上方是 "head40"),不应该合并 |
| 42 | + |
| 43 | +3. **缺乏合并策略控制**: |
| 44 | + - 当前只有 `automaticMergeHead` 布尔参数,只能开启/关闭自动合并 |
| 45 | + - 无法精细控制合并行为(例如:只允许水平合并、只允许垂直合并、只允许完整矩形合并等) |
| 46 | + |
| 47 | +## 解决方案设计 |
| 48 | + |
| 49 | +### 1. 引入 HeaderMergeStrategy 枚举 |
| 50 | + |
| 51 | +创建一个枚举类型来控制合并策略: |
| 52 | + |
| 53 | +```java |
| 54 | +public enum HeaderMergeStrategy { |
| 55 | + /** |
| 56 | + * 不进行任何自动合并 |
| 57 | + */ |
| 58 | + NONE, |
| 59 | + |
| 60 | + /** |
| 61 | + * 仅水平合并(同一行内的相同单元格) |
| 62 | + */ |
| 63 | + HORIZONTAL_ONLY, |
| 64 | + |
| 65 | + /** |
| 66 | + * 仅垂直合并(同一列内的相同单元格) |
| 67 | + */ |
| 68 | + VERTICAL_ONLY, |
| 69 | + |
| 70 | + /** |
| 71 | + * 仅完整的矩形区域合并(所有单元格必须形成完整的矩形且名称相同) |
| 72 | + */ |
| 73 | + FULL_RECTANGLE, |
| 74 | + |
| 75 | + /** |
| 76 | + * 自动合并(当前默认行为,向后兼容) |
| 77 | + */ |
| 78 | + AUTO |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +### 2. 增强 API 以支持合并策略配置 |
| 83 | + |
| 84 | +#### 2.1 在 WriteBasicParameter 中添加新字段 |
| 85 | + |
| 86 | +```java |
| 87 | +/** |
| 88 | + * 表头合并策略 |
| 89 | + */ |
| 90 | +private HeaderMergeStrategy headerMergeStrategy; |
| 91 | +``` |
| 92 | + |
| 93 | +#### 2.2 在 Builder 中添加配置方法 |
| 94 | + |
| 95 | +```java |
| 96 | +/** |
| 97 | + * 设置表头合并策略 |
| 98 | + * |
| 99 | + * @param strategy 合并策略 |
| 100 | + * @return this |
| 101 | + */ |
| 102 | +public T headerMergeStrategy(HeaderMergeStrategy strategy) { |
| 103 | + parameter().setHeaderMergeStrategy(strategy); |
| 104 | + return self(); |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +#### 2.3 保持向后兼容性 |
| 109 | + |
| 110 | +- 如果 `headerMergeStrategy` 为 `null`,则根据 `automaticMergeHead` 决定: |
| 111 | + - `automaticMergeHead == true` → `HeaderMergeStrategy.AUTO` |
| 112 | + - `automaticMergeHead == false` → `HeaderMergeStrategy.NONE` |
| 113 | +- 如果 `headerMergeStrategy` 不为 `null`,则使用新策略,忽略 `automaticMergeHead` |
| 114 | + |
| 115 | +### 3. 改进合并算法 |
| 116 | + |
| 117 | +#### 3.1 矩形区域有效性验证 |
| 118 | + |
| 119 | +在合并之前,需要验证: |
| 120 | +1. **矩形完整性**:确保矩形区域内的所有单元格都存在且名称相同 |
| 121 | +2. **上下文一致性**:对于垂直合并,需要验证上方单元格的上下文是否一致 |
| 122 | +3. **边界检查**:确保合并区域不超出表头边界 |
| 123 | + |
| 124 | +#### 3.2 不同策略的实现 |
| 125 | + |
| 126 | +- **NONE**: 不执行任何合并 |
| 127 | +- **HORIZONTAL_ONLY**: 只合并同一行内的相邻相同单元格 |
| 128 | +- **VERTICAL_ONLY**: 只合并同一列内的相邻相同单元格(需验证上下文) |
| 129 | +- **FULL_RECTANGLE**: 只合并完整的矩形区域(所有单元格名称相同) |
| 130 | +- **AUTO**: 保持当前行为(向后兼容),但添加矩形验证 |
| 131 | + |
| 132 | +### 4. 算法改进示例 |
| 133 | + |
| 134 | +#### 改进后的合并逻辑(FULL_RECTANGLE 策略) |
| 135 | + |
| 136 | +```java |
| 137 | +private boolean isValidRectangleRegion( |
| 138 | + List<Head> headList, |
| 139 | + int startRow, int endRow, |
| 140 | + int startCol, int endCol, |
| 141 | + String expectedName) { |
| 142 | + // 验证矩形区域内的所有单元格 |
| 143 | + for (int row = startRow; row <= endRow; row++) { |
| 144 | + for (int col = startCol; col <= endCol; col++) { |
| 145 | + if (row >= headList.get(col).getHeadNameList().size()) { |
| 146 | + return false; // 单元格不存在 |
| 147 | + } |
| 148 | + String cellName = headList.get(col).getHeadNameList().get(row); |
| 149 | + if (!expectedName.equals(cellName)) { |
| 150 | + return false; // 单元格名称不匹配 |
| 151 | + } |
| 152 | + } |
| 153 | + } |
| 154 | + return true; |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +#### 垂直合并的上下文验证 |
| 159 | + |
| 160 | +```java |
| 161 | +private boolean canMergeVertically( |
| 162 | + List<Head> headList, |
| 163 | + int row1, int row2, |
| 164 | + int col, |
| 165 | + String cellName) { |
| 166 | + // 检查上方单元格上下文是否一致 |
| 167 | + if (row1 > 0 && row2 > 0) { |
| 168 | + String upper1 = headList.get(col).getHeadNameList().get(row1 - 1); |
| 169 | + String upper2 = headList.get(col).getHeadNameList().get(row2 - 1); |
| 170 | + if (!Objects.equals(upper1, upper2)) { |
| 171 | + return false; // 上下文不一致,不能合并 |
| 172 | + } |
| 173 | + } |
| 174 | + return true; |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +## 实施步骤 |
| 179 | + |
| 180 | +### 阶段1:创建枚举和基本结构 |
| 181 | +1. 创建 `HeaderMergeStrategy` 枚举类 |
| 182 | +2. 在 `WriteBasicParameter` 中添加 `headerMergeStrategy` 字段 |
| 183 | +3. 在 `AbstractExcelWriterParameterBuilder` 中添加配置方法 |
| 184 | + |
| 185 | +### 阶段2:实现合并策略逻辑 |
| 186 | +1. 修改 `ExcelWriteHeadProperty.headCellRangeList()` 方法,支持不同策略 |
| 187 | +2. 实现矩形区域验证方法 |
| 188 | +3. 实现上下文验证方法 |
| 189 | + |
| 190 | +### 阶段3:向后兼容处理 |
| 191 | +1. 在 `AbstractWriteHolder` 中处理策略的默认值 |
| 192 | +2. 确保 `automaticMergeHead` 参数仍然有效 |
| 193 | + |
| 194 | +### 阶段4:测试和文档 |
| 195 | +1. 为 issue #666 创建测试用例 |
| 196 | +2. 添加其他策略的测试用例 |
| 197 | +3. 更新文档 |
| 198 | + |
| 199 | +## 代码修改点 |
| 200 | + |
| 201 | +### 需要修改的文件 |
| 202 | + |
| 203 | +1. **新建文件**: |
| 204 | + - `fesod/src/main/java/org/apache/fesod/excel/enums/HeaderMergeStrategy.java` |
| 205 | + |
| 206 | +2. **修改文件**: |
| 207 | + - `fesod/src/main/java/org/apache/fesod/excel/write/metadata/WriteBasicParameter.java` |
| 208 | + - `fesod/src/main/java/org/apache/fesod/excel/write/builder/AbstractExcelWriterParameterBuilder.java` |
| 209 | + - `fesod/src/main/java/org/apache/fesod/excel/write/property/ExcelWriteHeadProperty.java` |
| 210 | + - `fesod/src/main/java/org/apache/fesod/excel/write/metadata/holder/AbstractWriteHolder.java` |
| 211 | + - `fesod/src/main/java/org/apache/fesod/excel/write/metadata/holder/WriteHolder.java` |
| 212 | + - `fesod/src/main/java/org/apache/fesod/excel/context/WriteContextImpl.java` |
| 213 | + |
| 214 | +3. **测试文件**: |
| 215 | + - 新建测试用例验证 issue #666 的场景 |
| 216 | + - 添加各种策略的测试用例 |
| 217 | + |
| 218 | +## 预期效果 |
| 219 | + |
| 220 | +1. **修复 issue #666**:C2 和 D2 不再被错误合并 |
| 221 | +2. **增强灵活性**:用户可以根据需要选择不同的合并策略 |
| 222 | +3. **向后兼容**:现有的 `automaticMergeHead` 参数仍然有效 |
| 223 | +4. **提高准确性**:通过矩形验证,避免错误的合并 |
| 224 | + |
| 225 | +## 注意事项 |
| 226 | + |
| 227 | +1. **性能考虑**:矩形验证会增加一些计算开销,但影响应该很小 |
| 228 | +2. **边界情况**:需要处理空表头、单行表头、单列表头等特殊情况 |
| 229 | +3. **文档更新**:需要在用户文档中说明新的合并策略选项 |
0 commit comments