|
|
@@ -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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|