eec icon indicating copy to clipboard operation
eec copied to clipboard

模版导出咨询

Open SghSean opened this issue 2 months ago • 10 comments

能否在 TemplateSheet 中添加, 只复制原表表头数据,并且支持根据程序生成的字段对复制原表表头 实行定位追加功能, 同时支持,传入字段 绑定的模板 key, 传入最终导出数据,实现模板绑定导出。 列如 我有一个初始化模板,但是可以实行一定规则的追加数据,比如订单导入商品信息时,sku 可以不设限制追加, 然后给用户数据错误信息列

excel 文件

template_copy.xlsx

SghSean avatar Dec 18 '25 08:12 SghSean

先读到模板的样式放到Map里,Key是表头名,Value是样式,写的时候根据表头名拿到原来的样式并添加到新Excel里这样是不是满足你的需求?

wangguanquan avatar Dec 18 '25 09:12 wangguanquan

先读到模板的样式放到Map里,Key是表头名,Value是样式,写的时候根据表头名拿到原来的样式并添加到新Excel里这样是不是满足你的需求?

这样是可以满足需求, 我想说的是, 通过 TemplateSheet copy 原表的功能, 然后添加绑定数据的 key,最后传入导出的数据,写到 excel , 这个能在一个动作完成吗?

SghSean avatar Dec 19 '25 01:12 SghSean

先读到模板的样式放到Map里,Key是表头名,Value是样式,写的时候根据表头名拿到原来的样式并添加到新Excel里这样是不是满足你的需求?

中间 我修改过 TemplateSheet 代码, 通过 判断, row index 实现了复制原表的功能,这样实现的我需求, 但是要拆分成三步

  1. 复制原表 (包含样式, 数据验证) 到新表, 2. 读取新表 写入 绑定的 key 3. 读取新生成的模板表 导出错误数据

SghSean avatar Dec 19 '25 01:12 SghSean

没太理解需求,你是想数据上传做内容校验,NG的数据需要原样导出,是这样的需求吗?大概需要添加一列在最后并附带异常信息?

wangguanquan avatar Dec 19 '25 05:12 wangguanquan

没太理解需求,你是想数据上传做内容校验,NG的数据需要原样导出,是这样的需求吗?大概需要添加一列在最后并附带异常信息?

对,大概是这样的, 目前是我通过实现 自定义的 sheet 实现了 代码 /*

  • Copyright (c) 2017-2024, [email protected] All Rights Reserved.
  • Licensed under the Apache License, Version 2.0 (the "License");
  • you may not use this file except in compliance with the License.
  • You may obtain a copy of the License at
  • http://www.apache.org/licenses/LICENSE-2.0
    
  • Unless required by applicable law or agreed to in writing, software
  • distributed under the License is distributed on an "AS IS" BASIS,
  • WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  • See the License for the specific language governing permissions and
  • limitations under the License. */

package org.ttzero.excel.entity;

import org.dom4j.Document; import org.dom4j.Element; import org.ttzero.excel.entity.e7.XMLWorksheetWriter; import org.ttzero.excel.entity.style.Border; import org.ttzero.excel.entity.style.Fill; import org.ttzero.excel.entity.style.Font; import org.ttzero.excel.entity.style.NumFmt; import org.ttzero.excel.entity.style.Styles; import org.ttzero.excel.manager.Const; import org.ttzero.excel.reader.Cell; import org.ttzero.excel.reader.CellType; import org.ttzero.excel.reader.Col; import org.ttzero.excel.reader.CrossDimension; import org.ttzero.excel.reader.Dimension; import org.ttzero.excel.reader.ExcelReader; import org.ttzero.excel.reader.FullSheet; import org.ttzero.excel.util.FileUtil; import org.ttzero.excel.util.SAXReaderUtil; import org.ttzero.excel.util.StringUtil; import org.ttzero.excel.validation.Validation;

import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.BiFunction;

import org.ttzero.excel.entity.TemplateCopySheet.HeaderStyleModifier;

/**

  • 复制表头模板工作表,它支持从已有的Excel文件中复制表头及样式,然后通过Map映射表头名字和数据key,

  • 如果key不在表头中存在则在末尾新建一个表头列,支持修改表头样式,最后传入导出数据输出到新表。

  • 使用示例:

  • // 表头名称到数据key的映射

  • Map<String, String> headerMapping = new LinkedHashMap<>();

  • headerMapping.put("姓名", "name");

  • headerMapping.put("年龄", "age");

  • headerMapping.put("新增列", "newColumn"); // 不存在的列会自动添加

  • // 导出数据

  • List<Map<String, Object>> data = new ArrayList<>();

  • Map<String, Object> row = new HashMap<>();

  • row.put("name", "张三");

  • row.put("age", 25);

  • row.put("newColumn", "新数据");

  • data.add(row);

  • new Workbook()

  • .addSheet(new TemplateCopySheet(Paths.get("template.xlsx"))
    
  •     .setHeaderMapping(headerMapping)
    
  •     .setData(data))
    
  • .writeTo(Paths.get("output.xlsx"));
    
  • @author guanquan.wang at 2024-12-19 / public class TemplateCopySheet extends ListMapSheet<Object> { /*

    • 模板路径 / protected Path templatePath; /*
    • 模板流 / protected InputStream templateStream; /*
    • 读取模板用 / protected ExcelReader reader; /*
    • 源工作表索引 / protected int originalSheetIndex; /*
    • 源工作表名 / protected String originalSheetName; /*
    • 表头名称到数据key的映射
    • Key: 表头名称, Value: 数据key / protected Map<String, String> headerMapping; /*
    • 样式映射,缓存源样式索引映射到目标样式索引 / protected Map<Integer, Integer> styleMap; /*
    • 源表头信息
    • Key: 表头名称, Value: 列索引 / protected Map<String, Integer> originalHeaders; /*
    • 源表头样式
    • Key: 列索引, Value: 样式索引 / protected Map<Integer, Integer> originalHeaderStyles; /*
    • 源列宽 / protected Map<Integer, Double> originalColWidths; /*
    • 表头行号 / protected int headerRowNum = 1; /*
    • 是否已初始化 / protected boolean initialized; /*
    • 以Excel格式输出 / protected boolean writeAsExcel; /*
    • Delete the temp file if close buffer / protected boolean shouldDeleteTemp; /*
    • 自定义表头样式修改器 */ protected HeaderStyleModifier headerStyleModifier;

    /**

    • 表头样式修改器接口 / @FunctionalInterface public interface HeaderStyleModifier { /*
      • 修改表头样式
      • @param styles 样式表,可用于创建新的字体、填充、边框等
      • @param headerName 表头名称
      • @param style 原始样式
      • @return 新样式 */ int apply(Styles styles, String headerName, int style); }

    /**

    • 实例化模板复制工作表,默认以第一个工作表做为模板
    • @param templatePath 模板路径 */ public TemplateCopySheet(Path templatePath) { this(templatePath, 0); }

    /**

    • 实例化模板复制工作表,默认以第一个工作表做为模板
    • @param name 指定工作表名称
    • @param templatePath 模板路径 */ public TemplateCopySheet(String name, Path templatePath) { this(name, templatePath, 0); }

    /**

    • 实例化模板复制工作表并指定模板工作表索引
    • @param templatePath 模板路径
    • @param originalSheetIndex 指定源工作表索引(从0开始) */ public TemplateCopySheet(Path templatePath, int originalSheetIndex) { this(null, templatePath, originalSheetIndex); }

    /**

    • 实例化模板复制工作表并指定模板工作表索引
    • @param name 指定工作表名称
    • @param templatePath 模板路径
    • @param originalSheetIndex 指定源工作表索引(从0开始) */ public TemplateCopySheet(String name, Path templatePath, int originalSheetIndex) { super(name); this.templatePath = templatePath; this.originalSheetIndex = originalSheetIndex; }

    /**

    • 实例化模板复制工作表并指定模板工作表名
    • @param templatePath 模板路径
    • @param originalSheetName 指定源工作表名 */ public TemplateCopySheet(Path templatePath, String originalSheetName) { this(null, templatePath, originalSheetName); }

    /**

    • 实例化模板复制工作表并指定模板工作表名
    • @param name 指定工作表名称
    • @param templatePath 模板路径
    • @param originalSheetName 指定源工作表名 */ public TemplateCopySheet(String name, Path templatePath, String originalSheetName) { super(name); this.templatePath = templatePath; this.originalSheetName = originalSheetName; }

    /**

    • 实例化模板复制工作表,默认以第一个工作表做为模板
    • @param templateStream 模板输入流 */ public TemplateCopySheet(InputStream templateStream) { this(templateStream, 0); }

    /**

    • 实例化模板复制工作表,默认以第一个工作表做为模板
    • @param name 设置工作表名
    • @param templateStream 模板输入流 */ public TemplateCopySheet(String name, InputStream templateStream) { this(name, templateStream, 0); }

    /**

    • 实例化模板复制工作表并指定模板工作表索引
    • @param templateStream 模板输入流
    • @param originalSheetIndex 指定源工作表索引 */ public TemplateCopySheet(InputStream templateStream, int originalSheetIndex) { this(null, templateStream, originalSheetIndex); }

    /**

    • 实例化模板复制工作表并指定模板工作表名
    • @param templateStream 模板输入流
    • @param originalSheetName 指定源工作表名 */ public TemplateCopySheet(InputStream templateStream, String originalSheetName) { this(null, templateStream, originalSheetName); }

    /**

    • 实例化模板复制工作表并指定模板工作表索引
    • @param name 设置工作表名
    • @param templateStream 模板输入流
    • @param originalSheetIndex 指定源工作表索引 */ public TemplateCopySheet(String name, InputStream templateStream, int originalSheetIndex) { super(name); this.templateStream = templateStream; this.originalSheetIndex = originalSheetIndex; }

    /**

    • 实例化模板复制工作表并指定模板工作表名
    • @param name 设置工作表名
    • @param templateStream 模板输入流
    • @param originalSheetName 指定源工作表名 */ public TemplateCopySheet(String name, InputStream templateStream, String originalSheetName) { super(name); this.templateStream = templateStream; this.originalSheetName = originalSheetName; }

    /**

    • 设置表头映射
    • Key: 表头名称, Value: 数据Map中的key
    • @param headerMapping 表头映射
    • @return 当前工作表 */ public TemplateCopySheet setHeaderMapping(Map<String, String> headerMapping) { this.headerMapping = headerMapping; return this; }

    /**

    • 设置表头行号,默认为1
    • @param headerRowNum 表头行号(从1开始)
    • @return 当前工作表 */ public TemplateCopySheet setHeaderRowNum(int headerRowNum) { this.headerRowNum = headerRowNum; return this; }

    /**

    • 设置表头样式修改器,用于自定义修改表头样式
    • 使用示例:

    • .setHeaderStyleModifier((styles, headerName, style) -> {
    • if ("备注".equals(headerName)) {
      
    •     // 创建红色字体
      
    •     Font redFont = new Font("微软雅黑", 12, Font.Style.BOLD, Color.RED);
      
    •     // 创建灰色填充
      
    •     Fill greyFill = new Fill(PatternType.solid, new Color(217, 217, 217));
      
    •     // 组合样式
      
    •     return styles.addFont(redFont) | styles.addFill(greyFill) | Horizontals.CENTER;
      
    • }
      
    • return style;
      
    • })
    • @param headerStyleModifier 样式修改器,参数1为样式表,参数2为表头名称,参数3为原始样式,返回新样式
    • @return 当前工作表 */ public TemplateCopySheet setHeaderStyleModifier(HeaderStyleModifier headerStyleModifier) { this.headerStyleModifier = headerStyleModifier; return this; }

    @Override public TemplateCopySheet setData(List<Map<String, Object>> data) { super.setData(data); return this; }

    @Override public TemplateCopySheet setData(BiFunction<Integer, Map<String, Object>, List<Map<String, Object>>> dataSupplier) { super.setData(dataSupplier); return this; }

    /**

    • 获取表头列 */ @Override protected Column[] getHeaderColumns() { if (!headerReady) { try { initTemplate(); } catch (IOException e) { throw new ExcelWriteException("Failed to init template", e); } } return super.getHeaderColumns(); }

    /**

    • 初始化模板 */ protected void initTemplate() throws IOException { if (initialized) return; initialized = true;

      // 实例化ExcelReader if (templateStream != null) { templatePath = Files.createTempFile("eec-", null); if (templatePath == null) throw new IOException("Create temp directory error. Please check your permission"); java.io.OutputStream os = Files.newOutputStream(templatePath); FileUtil.cp(templateStream, os); FileUtil.close(os); FileUtil.close(templateStream); templateStream = null; shouldDeleteTemp = true; } if (templatePath != null) reader = ExcelReader.read(templatePath);

      // 查找源工作表 org.ttzero.excel.reader.Sheet[] sheets = reader.all(); if (StringUtil.isNotBlank(originalSheetName)) { int index = 0; for (; index < sheets.length && !originalSheetName.equals(sheets[index].getName()); index++) ; if (index >= sheets.length) throw new IOException("The original worksheet [" + originalSheetName + "] does not exist in template file."); originalSheetIndex = index; } else if (originalSheetIndex < 0 || originalSheetIndex >= sheets.length) { throw new IOException("The original worksheet index [" + originalSheetIndex + "] is out of range in template file[0-" + sheets.length + "]."); }

      // 加载模板工作表 FullSheet sheet = reader.sheet(originalSheetIndex).asFullSheet(); writeAsExcel = sheetWriter != null && XMLWorksheetWriter.class.isAssignableFrom(sheetWriter.getClass());

      // 样式缓存 styleMap = writeAsExcel ? new HashMap<>() : Collections.emptyMap();

      // 解析表头和样式 parseHeaderAndStyles(sheet);

      // 构建列信息 buildColumns(); }

    /**

    • 解析表头和样式 */ protected void parseHeaderAndStyles(FullSheet sheet) { originalHeaders = new LinkedHashMap<>(); originalHeaderStyles = new HashMap<>(); originalColWidths = new HashMap<>();

      // 获取列宽 List<Col> cols = sheet.getCols(); if (cols != null && !cols.isEmpty()) { for (Col col : cols) { for (int c = col.min; c <= col.max; c++) { originalColWidths.put(c - 1, col.width); } } }

      Styles styles0 = reader.getStyles();

      // 遍历表头行 for (Iterator<org.ttzero.excel.reader.Row> iter = sheet.iterator(); iter.hasNext(); ) { org.ttzero.excel.reader.Row row = iter.next(); if (row.getRowNum() == headerRowNum) { for (int i = row.getFirstColumnIndex(), end = row.getLastColumnIndex(); i < end; i++) { Cell cell = row.getCell(i); if (row.getCellType(cell) == CellType.STRING) { String headerName = row.getString(cell); if (StringUtil.isNotBlank(headerName)) { originalHeaders.put(headerName.trim(), i); originalHeaderStyles.put(i, cell.xf);

                       // 复制样式
                       if (writeAsExcel && !styleMap.containsKey(cell.xf)) {
                           styleMap.put(cell.xf, copyStyle(styles0, workbook.getStyles(), cell.xf));
                       }
                   }
               }
           }
           break;
       }
      

      }

      // 复制数据验证(包括级联列表) copyValidations(sheet); }

    /**

    • 复制数据验证 */ protected void copyValidations(FullSheet sheet) { if (!writeAsExcel) return;

      List<Validation> validations = sheet.getValidations(); if (validations == null || validations.isEmpty()) return;

      // 构建原列索引到新列索引的映射 Map<Integer, Integer> colIndexMapping = new HashMap<>(); if (headerMapping != null && !headerMapping.isEmpty()) { int newColIndex = 0; for (String headerName : headerMapping.keySet()) { Integer originalColIndex = originalHeaders.get(headerName); if (originalColIndex != null) { colIndexMapping.put(originalColIndex, newColIndex); } newColIndex++; } } else { // 无映射时,列索引保持不变 for (Map.Entry<String, Integer> entry : originalHeaders.entrySet()) { colIndexMapping.put(entry.getValue(), entry.getValue()); } }

      // 处理验证 List<Validation> newValidations = new ArrayList<>(); Map<String, String> refererSheetMap = new HashMap<>();

      for (Validation val : validations) { // 调整验证的维度范围 Validation newVal = adjustValidationDimension(val, colIndexMapping); if (newVal != null) { // 处理跨工作表引用 if (newVal.referer != null && newVal.referer.isCrossSheet()) { refererSheetMap.computeIfAbsent(newVal.referer.sheetName, k -> "__St" + getId() + "_val" + (System.currentTimeMillis() % 1000)); newVal.referer.sheetName = refererSheetMap.get(newVal.referer.sheetName); } newValidations.add(newVal); } }

      // 添加引用的工作表 if (!refererSheetMap.isEmpty()) { for (Map.Entry<String, String> entry : refererSheetMap.entrySet()) { workbook.addSheet(new TemplateSheet(templatePath, entry.getKey()).setName(entry.getValue()).hidden()); } // 读取defined_name try { Document document = SAXReaderUtil.createDefault().read(reader.getEntryStream("xl/workbook.xml")); Element definedNameEl = document.getRootElement().element("definedNames"); if (definedNameEl != null) { List<Element> sub = definedNameEl.elements(); Map<String, String> definedNames = new HashMap<>(sub.size()); for (Element e : sub) { String txt = e.getTextTrim(); int vt = Validation.testValueType(txt); CrossDimension cd; if (vt == 3 && (cd = CrossDimension.of(txt)).isCrossSheet() && refererSheetMap.containsKey(cd.sheetName)) { cd.sheetName = refererSheetMap.get(cd.sheetName); definedNames.put(e.attributeValue("name"), cd.toString()); } } if (!definedNames.isEmpty()) putExtProp(Const.ExtendPropertyKey.DEFINED_NAME, definedNames); } } catch (Exception ex) { // ignore } }

      if (!newValidations.isEmpty()) { putExtProp(Const.ExtendPropertyKey.DATA_VALIDATION, newValidations); } }

    /**

    • 调整验证的维度范围 */ protected Validation adjustValidationDimension(Validation val, Map<Integer, Integer> colIndexMapping) { if (val.sqrefList == null || val.sqrefList.isEmpty()) return val;

      List<Dimension> newSqrefList = new ArrayList<>(); for (Dimension dim : val.sqrefList) { // 获取新的列索引 Integer newFirstCol = colIndexMapping.get(dim.firstColumn - 1); Integer newLastCol = colIndexMapping.get(dim.lastColumn - 1);

       if (newFirstCol != null) {
           // 如果找到映射,使用新的列索引
           int fc = newFirstCol + 1;
           int lc = newLastCol != null ? newLastCol + 1 : fc;
           // 行号从表头行之后开始
           Dimension newDim = new Dimension(headerRowNum + 1, (short) fc, dim.lastRow, (short) lc);
           newSqrefList.add(newDim);
       } else {
           // 保持原维度(可能是全列验证)
           newSqrefList.add(dim);
       }
      

      }

      if (!newSqrefList.isEmpty()) { val.sqrefList = newSqrefList; return val; } return null; }

    /**

    • 复制样式 */ protected int copyStyle(Styles srcStyles, Styles distStyle, int copyXf) { int style = srcStyles.getStyleByIndex(copyXf), xf = 0; // 字体 Font font = srcStyles.getFont(style); if (font != null) xf |= distStyle.addFont(font.clone()); // 填充 Fill fill = srcStyles.getFill(style); if (fill != null) xf |= distStyle.addFill(fill.clone()); // 边框 Border border = srcStyles.getBorder(style); if (border != null) xf |= distStyle.addBorder(border.clone()); // 格式化 NumFmt numFmt = srcStyles.getNumFmt(style); if (numFmt != null) xf |= distStyle.addNumFmt(numFmt.clone()); // 水平对齐 xf |= srcStyles.getHorizontal(style); // 垂直对齐 xf |= srcStyles.getVertical(style); // 自动折行 xf |= srcStyles.getWrapText(style); return distStyle.of(xf); }

    /**

    • 构建列信息 */ protected void buildColumns() { List<Column> columnList = new ArrayList<>();

      if (headerMapping == null || headerMapping.isEmpty()) { // 没有映射,使用原表头作为key for (Map.Entry<String, Integer> entry : originalHeaders.entrySet()) { String headerName = entry.getKey(); int colIndex = entry.getValue(); Column col = createColumnFromTemplate(headerName, headerName, colIndex); columnList.add(col); } } else { // 根据映射构建列 for (Map.Entry<String, String> entry : headerMapping.entrySet()) { String headerName = entry.getKey(); String dataKey = entry.getValue();

           Integer colIndex = originalHeaders.get(headerName);
           Column col;
           if (colIndex != null) {
               // 表头存在,使用原样式
               col = createColumnFromTemplate(headerName, dataKey, colIndex);
           } else {
               // 表头不存在,在末尾新建
               col = createNewColumn(headerName, dataKey);
           }
           columnList.add(col);
       }
      

      }

      columns = columnList.toArray(new Column[0]); }

    /**

    • 从模板创建列 */ protected Column createColumnFromTemplate(String headerName, String dataKey, int colIndex) { Column col = new Column(headerName, dataKey);

      // 设置列宽 Double width = originalColWidths.get(colIndex); if (width != null && width > 0) { col.setWidth(width); }

      // 设置表头样式 Integer originalStyleXf = originalHeaderStyles.get(colIndex); if (originalStyleXf != null && writeAsExcel) { Integer newStyleIndex = styleMap.get(originalStyleXf); if (newStyleIndex != null) { int style = workbook.getStyles().getStyleByIndex(newStyleIndex); // 应用样式修改器 if (headerStyleModifier != null) { style = headerStyleModifier.apply(workbook.getStyles(), headerName, style); } col.setHeaderStyle(style); } }

      return col; }

    /**

    • 创建新列 */ protected Column createNewColumn(String headerName, String dataKey) { Column col = new Column(headerName, dataKey);

      // 使用默认表头样式或第一个表头的样式 if (!originalHeaderStyles.isEmpty() && writeAsExcel) { Integer firstStyleXf = originalHeaderStyles.values().iterator().next(); Integer newStyleIndex = styleMap.get(firstStyleXf); if (newStyleIndex != null) { int style = workbook.getStyles().getStyleByIndex(newStyleIndex); // 应用样式修改器 if (headerStyleModifier != null) { style = headerStyleModifier.apply(workbook.getStyles(), headerName, style); } col.setHeaderStyle(style); } }

      return col; }

    @Override public void close() throws IOException { super.close(); if (reader != null) { reader.close(); reader = null; } if (templateStream != null) { templateStream.close(); templateStream = null; } if (shouldDeleteTemp && templatePath != null) { FileUtil.rm(templatePath); } } }

SghSean avatar Dec 19 '25 05:12 SghSean

没太理解需求,你是想数据上传做内容校验,NG的数据需要原样导出,是这样的需求吗?大概需要添加一列在最后并附带异常信息?

很多都是原代码的 copy ,所以想着 能否在原有 模板导出做增强处理

SghSean avatar Dec 19 '25 05:12 SghSean

没太理解需求,你是想数据上传做内容校验,NG的数据需要原样导出,是这样的需求吗?大概需要添加一列在最后并附带异常信息?

很多都是原代码的 copy ,所以想着 能否在原有 模板导出做增强处理

增加addColumn(int startRow, int colIndex, Column column)方法是不是可以,在指定位置添加列,可以通过Column指定样式,未指定时使用前一个单元格的样式

wangguanquan avatar Dec 19 '25 06:12 wangguanquan

没太理解需求,你是想数据上传做内容校验,NG的数据需要原样导出,是这样的需求吗?大概需要添加一列在最后并附带异常信息?

很多都是原代码的 copy ,所以想着 能否在原有 模板导出做增强处理

增加addColumn(int startRow, int colIndex, Column column)方法是不是可以,在指定位置添加列,可以通过Column指定样式,未指定时使用前一个单元格的样式

嗯,这个是复制了全部数据,可以操作, 但需求是, 正确的数据落库, 错误的数据返回, 此时相当于原表数据是丢弃的, 给了一个新的 数据集合进去

SghSean avatar Dec 19 '25 06:12 SghSean

没太理解需求,你是想数据上传做内容校验,NG的数据需要原样导出,是这样的需求吗?大概需要添加一列在最后并附带异常信息?

很多都是原代码的 copy ,所以想着 能否在原有 模板导出做增强处理

增加addColumn(int startRow, int colIndex, Column column)方法是不是可以,在指定位置添加列,可以通过Column指定样式,未指定时使用前一个单元格的样式

测试用例

@Test public void testTemplateCopySheet() throws IOException {
    Path templatePath = Paths.get("./parcel_outbound_import_template_1765422494020.xlsx");
    // 如果模板文件不存在则跳过测试
    if (!Files.exists(templatePath)) {
        System.out.println("Template file not found, skip test: " + templatePath);
        return;
    }

    Map<String, String> headerMapping = new LinkedHashMap<>();

    final String fileName = "template copy sheet test.xlsx";
    try (ExcelReader read = ExcelReader.read(templatePath)) {
        List<Map<String, Object>> maps = read.sheet(0).dataRows().map(Row::toMap).collect(Collectors.toList());

        Map<String, Object> map = maps.get(0);
        map.forEach((k,v) -> headerMapping.put(k, k));

    }
    // 新增一个原表不存在的列
    headerMapping.put("备注", "备注");

    // 准备测试数据
    List<Map<String, Object>> data = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        Map<String, Object> row = new HashMap<>();
        row.put("参考单号", "REF" + (1000 + i));
        row.put("平台单号", "PLT" + (2000 + i));
        row.put("销售平台", "淘宝");
        row.put("仓库", "深圳仓");
        row.put("物流渠道", "标准快递");
        row.put("物流承运商", "顺丰速运");
        row.put("物流追踪号", "SF" + System.currentTimeMillis() + i);
        row.put("国家", "中国");
        row.put("州省", "广东省");
        row.put("城市", "深圳市");
        row.put("详细地址", "南山区科技园" + i + "号");
        row.put("邮编", "518000");
        row.put("收件人", "张三" + i);
        row.put("电话", "1380000" + String.format("%04d", i));
        row.put("SKU1", "SKU-A" + (3000 + i));
        row.put("出库数量1", i + 1);
        row.put("SKU2", "SKU-B" + (4000 + i));
        row.put("出库数量2", i + 2);
        row.put("备注", "备注信息" + i);
        data.add(row);
    }


    new Workbook()
        .addSheet(new TemplateCopySheet(templatePath)
            .setHeaderMapping(headerMapping)
            .setData(data)
            // 修改表头样式
            .setHeaderStyleModifier((styles, headerName, style) -> {
                // 新增列使用红色字体 + 灰色背景
                if ("备注".equals(headerName)) {
                    Font redFont = new Font("微软雅黑", 12, Font.Style.BOLD, Color.RED);
                    Fill greyFill = new Fill(PatternType.solid, new Color(217, 217, 217));
                    return styles.addFont(redFont) | styles.addFill(greyFill) | Horizontals.CENTER;
                }
                return style;
            }))
        .writeTo(defaultTestPath.resolve(fileName));

    // 验证导出结果
    try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve(fileName))) {
        FullSheet sheet = reader.sheet(0).asFullSheet();
        Iterator<Row> iter = sheet.header(1).iterator();

        // 验证数据行数
        int count = 0;
        while (iter.hasNext()) {
            Row row = iter.next();
            if (!row.isBlank()) {
                count++;
            }
        }
        assertEquals(data.size(), count);
    }
}

SghSean avatar Dec 19 '25 06:12 SghSean

厉害👍,之前有一个ISSUE在导入异常时添加TIPS的需求,一边检查一边写异常数据:https://github.com/wangguanquan/eec/issues/383

wangguanquan avatar Dec 19 '25 08:12 wangguanquan

厉害👍,之前有一个ISSUE在导入异常时添加TIPS的需求,一边检查一边写异常数据:#383

后续会迭代,这个功能嘛?

SghSean avatar Dec 22 '25 01:12 SghSean

可以考虑这种场景还是比较多的,年底开发任务比较重,短期内不会实现

wangguanquan avatar Dec 23 '25 11:12 wangguanquan