hanchaolong недель назад: 2
Родитель
Сommit
6986adb6f4

+ 364 - 0
jd-logistics-modules/jd-logistics-system/src/main/java/com/ruoyi/logistics/handler/JDOrderDynamicTemplateHandler.java

@@ -0,0 +1,364 @@
+package com.ruoyi.logistics.handler;
+
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.write.handler.RowWriteHandler;
+import com.alibaba.excel.write.handler.SheetWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
+import com.ruoyi.logistics.domain.dto.JDOrderDTO;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddressList;
+import org.apache.poi.xssf.usermodel.XSSFDataValidation;
+import org.apache.poi.ss.usermodel.DataValidation.ErrorStyle;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JDOrder 动态模板处理器
+ * 使用 EasyExcel 框架实现带下拉框的 Excel 模板导出
+ * 
+ * @author RuiJing
+ * @date 2026-03-27
+ */
+public class JDOrderDynamicTemplateHandler {
+
+    /**
+     * 导出带动态下拉框的 Excel 模板
+     * 
+     * @param response HTTP 响应对象
+     * @param productCodes 产品类型下拉列表数据
+     * @param goodsNames 物品名称下拉列表数据
+     * @throws IOException IO 异常
+     */
+    public void exportDynamicTemplate(HttpServletResponse response, 
+                                      List<String> productCodes, 
+                                      List<String> goodsNames) throws IOException {
+        
+        // 设置响应头
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode("批量下单模板", "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+
+        // 创建 Excel 写入器,注册自定义 SheetWriteHandler 和 RowWriteHandler
+        EasyExcel.write(response.getOutputStream(), JDOrderDTO.class)
+                .sheet("订单")
+                .registerWriteHandler(createDropdownSheetHandler(productCodes, goodsNames))
+                .registerWriteHandler(createTextFormatRowHandler())
+                .doWrite(ArrayList::new);
+    }
+
+    /**
+     * 创建下拉框数据验证处理器
+     * 
+     * @param productCodes 产品类型下拉列表数据
+     * @param goodsNames 物品名称下拉列表数据
+     * @return SheetWriteHandler 处理器
+     */
+    private SheetWriteHandler createDropdownSheetHandler(List<String> productCodes, 
+                                                         List<String> goodsNames) {
+        return new SheetWriteHandler() {
+            @Override
+            public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, 
+                                        WriteSheetHolder writeSheetHolder) {
+                Sheet sheet = writeSheetHolder.getSheet();
+                Workbook workbook = writeWorkbookHolder.getWorkbook();
+
+                // 1. 为"物品名称"列(第 G 列,索引 6)添加下拉框
+                if (goodsNames != null && !goodsNames.isEmpty()) {
+                    addDropdownValidation(workbook, sheet, goodsNames, 6, "GoodsNames");
+                }
+                
+                // 2. 为"产品类型"列(第 M 列,索引 12)添加下拉框
+                if (productCodes != null && !productCodes.isEmpty()) {
+                    addDropdownValidation(workbook, sheet, productCodes, 12, "ProductCodes");
+                }
+
+                // 3. 为"包装服务"列(第 N 列,索引 13)添加下拉框
+                addDropdownValidation(workbook, sheet, getYesNoOptions(), 13, "YesOrNo");
+                
+                // 4. 为"签单返还"列(第 P 列,索引 15)添加下拉框(复用 YesOrNo)
+                addDropdownValidationToExistingRange(workbook, sheet, 15, "YesOrNo");
+                
+                // 5. 添加数值列的数据验证(不能为负数)
+                // 物品重量(第 H 列,索引 7)
+                addNumberValidation(workbook, sheet, 7, "物品重量");
+                // 物品体积(第 I 列,索引 8)
+                addNumberValidation(workbook, sheet, 8, "物品体积");
+                // 物品数量(第 J 列,索引 9)
+                addNumberValidation(workbook, sheet, 9, "物品数量");
+                // 保价金额(第 O 列,索引 14)
+                addNumberValidation(workbook, sheet, 14, "保价金额");
+                
+                // 6. 添加时间列的数据验证(格式:yyyy-MM-dd HH:00:00)
+                // 上门取件开始时间(第 K 列,索引 10)
+                addDateTimeValidation(workbook, sheet, 10, "上门取件开始时间");
+                // 上门取件结束时间(第 L 列,索引 11)
+                addDateTimeValidation(workbook, sheet, 11, "上门取件结束时间");
+            }
+        };
+    }
+
+    /**
+     * 添加下拉框数据验证
+     *
+     * @param workbook 工作簿对象
+     * @param sheet 工作表对象
+     * @param dataList 下拉选项数据列表
+     * @param columnIndex 列索引(从 0 开始)
+     * @param rangeName 命名区域名称
+     */
+    private void addDropdownValidation(Workbook workbook, Sheet sheet,
+                                       List<String> dataList,
+                                       int columnIndex,
+                                       String rangeName) {
+        if (dataList == null || dataList.isEmpty()) {
+            return;
+        }
+
+        // 检查是否已存在该命名区域
+        Name existingName = workbook.getName(rangeName);
+        if (existingName == null) {
+            // 1. 创建隐藏的工作表存储下拉选项数据
+            String hiddenSheetName = "_hidden_" + rangeName;
+            Sheet hiddenSheet = workbook.createSheet(hiddenSheetName);
+
+            // 填充数据到隐藏工作表的第 1 列
+            for (int i = 0; i < dataList.size(); i++) {
+                Row row = hiddenSheet.createRow(i);
+                Cell cell = row.createCell(0);
+                cell.setCellValue(dataList.get(i));
+            }
+
+            // 隐藏该工作表
+            workbook.setSheetHidden(workbook.getSheetIndex(hiddenSheet), true);
+
+            // 2. 创建命名区域
+            Name namedCell = workbook.createName();
+            namedCell.setNameName(rangeName);
+            String formula = hiddenSheet.getSheetName() + "!$A$1:$A$" + dataList.size();
+            namedCell.setRefersToFormula(formula);
+        }
+
+        // 3. 设置数据验证规则
+        DataValidationHelper helper = sheet.getDataValidationHelper();
+        DataValidationConstraint constraint = helper.createFormulaListConstraint(rangeName);
+
+        // 设置下拉框应用范围(从第 2 行开始,第 1 行是标题)
+        CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, columnIndex, columnIndex);
+
+        DataValidation validation = helper.createValidation(constraint, addressList);
+        configureValidation(validation);
+
+        sheet.addValidationData(validation);
+    }
+
+    /**
+     * 为已有命名区域的列添加下拉框数据验证
+     *
+     * @param workbook 工作簿对象
+     * @param sheet 工作表对象
+     * @param columnIndex 列索引(从 0 开始)
+     * @param rangeName 已存在的命名区域名称
+     */
+    private void addDropdownValidationToExistingRange(Workbook workbook, Sheet sheet,
+                                                       int columnIndex,
+                                                       String rangeName) {
+        // 验证命名区域是否存在
+        Name existingName = workbook.getName(rangeName);
+        if (existingName == null) {
+            return; // 如果命名区域不存在,直接返回
+        }
+
+        // 设置数据验证规则
+        DataValidationHelper helper = sheet.getDataValidationHelper();
+        DataValidationConstraint constraint = helper.createFormulaListConstraint(rangeName);
+
+        // 设置下拉框应用范围(从第 2 行开始,第 1 行是标题)
+        CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, columnIndex, columnIndex);
+
+        DataValidation validation = helper.createValidation(constraint, addressList);
+        configureValidation(validation);
+
+        sheet.addValidationData(validation);
+    }
+
+    /**
+     * 配置数据验证的通用属性
+     *
+     * @param validation 数据验证对象
+     */
+    private void configureValidation(DataValidation validation) {
+        // 允许空白输入
+        validation.setSuppressDropDownArrow(true);
+        validation.setShowErrorBox(true);
+        validation.setShowPromptBox(true);
+
+        // 处理 POI 的兼容性问题
+        if (validation instanceof XSSFDataValidation) {
+            ((XSSFDataValidation) validation).setSuppressDropDownArrow(true);
+            ((XSSFDataValidation) validation).setShowErrorBox(true);
+        }
+    }
+
+
+    /**
+     * 获取是/否选项下拉列表
+     */
+    private List<String> getYesNoOptions() {
+        return java.util.Arrays.asList("是", "否");
+    }
+
+    /**
+     * 添加数值数据验证(必须大于 0)
+     *
+     * @param workbook 工作簿对象
+     * @param sheet 工作表对象
+     * @param columnIndex 列索引(从 0 开始)
+     * @param fieldName 字段名称(用于错误提示)
+     */
+    private void addNumberValidation(Workbook workbook, Sheet sheet,
+                                     int columnIndex, String fieldName) {
+        DataValidationHelper helper = sheet.getDataValidationHelper();
+
+        // 使用自定义公式验证:数值必须大于 0
+        String excelCol = columnToLetter(columnIndex);
+        String formula = excelCol + "2>0";
+
+        DataValidationConstraint constraint = helper.createCustomConstraint(formula);
+
+        // 设置验证范围(从第 2 行开始,第 1 行是标题)
+        CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, columnIndex, columnIndex);
+
+        DataValidation validation = helper.createValidation(constraint, addressList);
+
+        // 设置错误提示
+        validation.setErrorStyle(ErrorStyle.STOP);
+        validation.createErrorBox("输入值无效", fieldName + "必须大于 0,请输入正数!");
+        validation.setShowErrorBox(true);
+
+        // 允许空白输入
+        validation.setSuppressDropDownArrow(true);
+
+        if (validation instanceof XSSFDataValidation) {
+            ((XSSFDataValidation) validation).setSuppressDropDownArrow(true);
+            ((XSSFDataValidation) validation).setShowErrorBox(true);
+        }
+
+        sheet.addValidationData(validation);
+    }
+
+    /**
+     * 添加日期时间数据验证(格式:yyyy-MM-dd HH:00:00)
+     *
+     * @param workbook 工作簿对象
+     * @param sheet 工作表对象
+     * @param columnIndex 列索引(从 0 开始)
+     * @param fieldName 字段名称(用于错误提示)
+     */
+    private void addDateTimeValidation(Workbook workbook, Sheet sheet,
+                                       int columnIndex, String fieldName) {
+        DataValidationHelper helper = sheet.getDataValidationHelper();
+
+        // 将列索引转换为 Excel 列字母(如 K、L)
+        String excelCol = columnToLetter(columnIndex);
+        
+        // 构建验证公式:严格按照 yyyy-MM-dd HH:00:00 格式
+        // 验证要点:
+        // 1. 总长度必须为 19 个字符
+        // 2. 固定位置的字符必须是:- - 空格 : :
+        // 3. 分钟 (15-16 位) 和秒 (18-19 位) 必须是 00
+        // 4. 年、月、日、时必须是数字
+        String formula = "AND(" +
+            "LEN(" + excelCol + "2)=19," +                                    // 长度=19
+            "MID(" + excelCol + "2,5,1)=\"-\"," +                             // 第 5 位=-
+            "MID(" + excelCol + "2,8,1)=\"-\"," +                             // 第 8 位=-
+            "MID(" + excelCol + "2,11,1)=\" \"," +                            // 第 11 位=空格
+            "MID(" + excelCol + "2,14,1)=\":\"," +                            // 第 14 位=:
+            "MID(" + excelCol + "2,17,1)=\":\"," +                            // 第 17 位=:
+            "MID(" + excelCol + "2,15,2)=\"00\"," +                           // 分钟=00
+            "MID(" + excelCol + "2,18,2)=\"00\"," +                           // 秒=00
+            "--MID(" + excelCol + "2,1,4)>=1900," +                           // 年>=1900
+            "--MID(" + excelCol + "2,1,4)<=9999," +                           // 年<=9999
+            "--MID(" + excelCol + "2,6,2)>=1," +                              // 月>=1
+            "--MID(" + excelCol + "2,6,2)<=12," +                             // 月<=12
+            "--MID(" + excelCol + "2,9,2)>=1," +                              // 日>=1
+            "--MID(" + excelCol + "2,9,2)<=31," +                             // 日<=31
+            "--MID(" + excelCol + "2,12,2)>=0," +                             // 时>=0
+            "--MID(" + excelCol + "2,12,2)<=23" +                             // 时<=23
+            ")";
+
+        DataValidationConstraint constraint = helper.createCustomConstraint(formula);
+
+        // 设置验证范围(从第 2 行开始,第 1 行是标题)
+        CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, columnIndex, columnIndex);
+
+        DataValidation validation = helper.createValidation(constraint, addressList);
+
+        // 设置输入提示,引导用户输入格式
+        validation.createPromptBox(fieldName + "输入提示", "请输入日期和时间,格式:yyyy-MM-dd HH:00:00(例如:2026-01-01 10:00:00)");
+        validation.setShowPromptBox(true);
+
+        // 设置错误提示
+        validation.setErrorStyle(ErrorStyle.STOP);
+        validation.createErrorBox("输入值无效", fieldName + "格式不正确,请严格按照 yyyy-MM-dd HH:00:00 格式输入(分钟和秒固定为 00)!");
+        validation.setShowErrorBox(true);
+
+        // 允许空白输入
+        validation.setSuppressDropDownArrow(true);
+
+        if (validation instanceof XSSFDataValidation) {
+            ((XSSFDataValidation) validation).setSuppressDropDownArrow(true);
+            ((XSSFDataValidation) validation).setShowErrorBox(true);
+        }
+
+        sheet.addValidationData(validation);
+    }
+
+    /**
+     * 将列索引转换为 Excel 列字母
+     * 
+     * @param columnIndex 列索引(从 0 开始)
+     * @return Excel 列字母
+     */
+    private String columnToLetter(int columnIndex) {
+        StringBuilder result = new StringBuilder();
+        int index = columnIndex + 1; // 转换为从 1 开始
+        while (index > 0) {
+            index--;
+            result.insert(0, (char) ('A' + index % 26));
+            index /= 26;
+        }
+        return result.toString();
+    }
+
+    /**
+     * 创建文本格式的 RowWriteHandler
+     * 用于设置上门取件开始时间和结束时间列为文本格式
+     *
+     * @return RowWriteHandler 处理器
+     */
+    private SheetWriteHandler createTextFormatRowHandler() {
+        return new SheetWriteHandler() {
+            @Override
+            public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
+                Sheet sheet = writeSheetHolder.getSheet();
+                DataFormat dataFormat = writeWorkbookHolder.getWorkbook().createDataFormat();
+
+                // 创建文本格式单元格样式
+                CellStyle textCellStyle = writeWorkbookHolder.getWorkbook().createCellStyle();
+                textCellStyle.setDataFormat(dataFormat.getFormat("@")); // 等同于setDataFormat(TEXT_FORMAT_CODE)
+
+                // 为sheet的所有列设置默认文本格式(这里设置前100列,可根据需求调整范围)
+                int maxColumnNum = 100;
+                for (int i = 0; i < maxColumnNum; i++) {
+                    sheet.setDefaultColumnStyle(i, textCellStyle);
+                }
+            }
+        };
+    }
+}