xiaoyi
8 天以前 68317d44b233f28150336bbf3aa3a32400552437
fix(excel): 导入模板日期改为文本显示,修复显示为 Excel 序列号

## 问题
22 个导入模板的日期列显示为 Excel 序列号,使用者看不懂该填什么格式:
员工入职日期 43891.0
员工合同签订日 46023.0
考勤打卡时间 46037.354166666664

## 排查过程
1. 先尝试注册自定义 LocalDate/LocalDateTime 转换器 —— 无效。
原因:FastExcel 内置同名转换器(cn.idev.excel.converters.localdate.LocalDateStringConverter),
写出时优先命中内置的 Number 转换器,自定义转换器不生效。
2. 最终改用 CellWriteHandler:单元格写完后介入,
读取 context.getOriginalValue(),把 LocalDate/LocalDateTime 格式化成文本写入。

## 实现
新增 DateAsTextCellWriteHandler:
- afterCellDispose 里拦截,跳过表头行
- LocalDate -> yyyy-MM-dd
- LocalDateTime -> yyyy-MM-dd HH:mm:ss
- java.util.Date -> yyyy-MM-dd
- cell.setCellType(STRING) + setCellValue(text)

ExcelUtils 新增 writeTemplate 方法(专用模板写出):
- 注册 DateAsTextCellWriteHandler
- 22 个 Controller 的 importTemplate 全部改用 writeTemplate
- 导入解析仍走 ExcelUtils.read,不受影响

## 顺带修复模板示例数据
- 用户:部门编号改为部门名称(取真实部门)
- 部门:上级部门改为真实根部门(原「总公司」不存在)
- 员工:部门/岗位取真实数据(原「研发部」不存在)
- 客户:示例改为「示例客户A/B(请替换)」(原「超级管理员」「源码」是芋道 demo)
- MDM物料:分类编码/单位编码补真实值(原为空,必填项)

## 验证(本地实测)
22 个模板逐一检查,日期序列号 0 个:
员工入职日期 2020-03-01 出生 1990-01-15
员工合同 2026-01-01 ~ 2028-12-31
考勤日期 2026-01-15 08:30:00
已添加3个文件
已修改24个文件
305 ■■■■■ 文件已修改
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalDateStringConverter.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalDateTimeStringConverter.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/DateAsTextCellWriteHandler.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/MdmUnitMeasureController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/MesDvMachineryController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/MesDvMeteringController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/MesMdItemTypeController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/MesQcIpqcController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/iqc/MesQcIqcController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/supplier/SrmSupplierController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/PostController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalDateStringConverter.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.framework.excel.core.convert;
import cn.idev.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
 * LocalDate å†™å‡ºä¸ºçº¯æ–‡æœ¬ï¼ˆyyyy-MM-dd)的转换器
 *
 * é»˜è®¤ FastExcel æŠŠæ—¥æœŸå†™æˆã€Œæ—¥æœŸå•元格」,在 Excel é‡Œæ˜¾ç¤ºä¸º 46023.0 è¿™ç±»åºåˆ—号,
 * ä½¿ç”¨è€…看不懂该填什么格式。导入模板类场景注册本转换器后,日期以文本输出,直观可读。
 *
 * æ³¨æ„ï¼šä»…用于「写出模板」,不要用于导入解析。
 */
public class LocalDateStringConverter implements Converter<LocalDate> {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    @Override
    public Class<LocalDate> supportJavaTypeKey() {
        return LocalDate.class;
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }
    @Override
    public WriteCellData<?> convertToExcelData(LocalDate value, ExcelContentProperty contentProperty,
                                               GlobalConfiguration globalConfiguration) {
        return new WriteCellData<>(value.format(FORMATTER));
    }
}
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalDateTimeStringConverter.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.framework.excel.core.convert;
import cn.idev.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
 * LocalDateTime å†™å‡ºä¸ºçº¯æ–‡æœ¬ï¼ˆyyyy-MM-dd HH:mm:ss)的转换器
 *
 * ç”¨é€”同 {@link LocalDateStringConverter},避免时间显示为 Excel åºåˆ—号。
 */
public class LocalDateTimeStringConverter implements Converter<LocalDateTime> {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    @Override
    public Class<LocalDateTime> supportJavaTypeKey() {
        return LocalDateTime.class;
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }
    @Override
    public WriteCellData<?> convertToExcelData(LocalDateTime value, ExcelContentProperty contentProperty,
                                               GlobalConfiguration globalConfiguration) {
        return new WriteCellData<>(value.format(FORMATTER));
    }
}
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/DateAsTextCellWriteHandler.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package cn.iocoder.yudao.framework.excel.core.handler;
import cn.idev.excel.write.handler.CellWriteHandler;
import cn.idev.excel.write.handler.context.CellWriteHandlerContext;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
 * æŠŠæ—¥æœŸå•元格强制写成纯文本的处理器
 *
 * èƒŒæ™¯ï¼šFastExcel åœ¨å†™å‡ºæ—¶ï¼Œå³ä½¿æ³¨å†Œäº†å­—符串转换器,日期字段仍可能被内置的
 * Number è½¬æ¢å™¨æŽ¥ç®¡ï¼Œæœ€ç»ˆåœ¨ Excel é‡Œæ˜¾ç¤ºä¸º 46023.0 è¿™ç±»åºåˆ—号,使用者看不懂。
 *
 * æœ¬å¤„理器在单元格写完后介入,把 LocalDate / LocalDateTime ç±»åž‹çš„原始值
 * æ ¼å¼åŒ–为 yyyy-MM-dd / yyyy-MM-dd HH:mm:ss å¹¶ä»¥æ–‡æœ¬å†™å…¥ã€‚
 *
 * ä»…用于「下载导入模板」,不影响导入解析。
 */
public class DateAsTextCellWriteHandler implements CellWriteHandler {
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    @Override
    public void afterCellDispose(CellWriteHandlerContext context) {
        // åªå¤„理数据行,跳过表头
        if (Boolean.TRUE.equals(context.getHead())) {
            return;
        }
        Object value = context.getOriginalValue();
        if (value == null) {
            return;
        }
        String text = null;
        if (value instanceof LocalDate localDate) {
            text = localDate.format(DATE_FORMATTER);
        } else if (value instanceof LocalDateTime localDateTime) {
            text = localDateTime.format(DATETIME_FORMATTER);
        } else if (value instanceof java.util.Date date) {
            text = date.toInstant().atZone(java.time.ZoneId.systemDefault())
                    .toLocalDate().format(DATE_FORMATTER);
        }
        if (text == null) {
            return;
        }
        Cell cell = context.getCell();
        if (cell == null) {
            return;
        }
        cell.setCellType(CellType.STRING);
        cell.setCellValue(text);
    }
}
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java
@@ -9,7 +9,10 @@
import cn.idev.excel.event.AnalysisEventListener;
import cn.idev.excel.metadata.CellExtra;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.excel.core.convert.LocalDateStringConverter;
import cn.iocoder.yudao.framework.excel.core.convert.LocalDateTimeStringConverter;
import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy;
import cn.iocoder.yudao.framework.excel.core.handler.DateAsTextCellWriteHandler;
import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;
@@ -66,6 +69,32 @@
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
    }
    /**
     * è¾“出 Excel æ¨¡æ¿ï¼ˆæ—¥æœŸæŒ‰æ–‡æœ¬æ¸²æŸ“)
     *
     * ä¸Ž {@link #write} çš„区别:额外注册了 {@link LocalDateStringConverter},
     * æŠŠ LocalDate / LocalDateTime å†™æˆ yyyy-MM-dd æ–‡æœ¬ï¼Œè€Œä¸æ˜¯ Excel æ—¥æœŸåºåˆ—号。
     * ä»…用于「下载导入模板」,导入解析仍走 {@link #read},不受影响。
     */
    public static <T> void writeTemplate(HttpServletResponse response, String filename, String sheetName,
                                         Class<T> head, List<T> data) throws IOException {
        ExcelWriter excelWriter = FastExcelFactory.write(response.getOutputStream(), head)
                .autoCloseStream(false)
                .registerWriteHandler(new ColumnWidthMatchStyleStrategy())
                .registerWriteHandler(new SelectSheetWriteHandler(head))
                .registerWriteHandler(new DateAsTextCellWriteHandler())
                .registerConverter(new LongStringConverter())
                .build();
        try {
            excelWriter.write(data, FastExcelFactory.writerSheet(sheetName).build());
        } finally {
            excelWriter.finish();
            excelWriter.close();
        }
        response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
    }
    public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
        // å‚考 https://t.zsxq.com/zM77F å¸–子,增加 try å¤„理,兼容 windows åœºæ™¯
        try (InputStream inputStream = file.getInputStream()) {
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
@@ -221,7 +221,7 @@
                        .productCode("ITEM001").productName("示例产品A")
                        .count(new BigDecimal("100")).contractPrice(new BigDecimal("20.00")).remark("示例数据,导入前请替换")
                        .build());
        ExcelUtils.write(response, "合同导入模板.xls", "合同", CrmContractImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "合同导入模板.xls", "合同", CrmContractImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -281,17 +281,17 @@
    @GetMapping("/get-import-template")
    @Operation(summary = "获得导入客户模板")
    public void importTemplate(HttpServletResponse response) throws IOException {
        // æ‰‹åŠ¨åˆ›å»ºå¯¼å‡º demo
        // ç¤ºä¾‹å®¢æˆ·ï¼šåç§°/手机/邮箱均使用占位值,避免照抄官方 demo äº§ç”Ÿå‡æ•°æ®
        List<CrmCustomerImportExcelVO> list = Arrays.asList(
                CrmCustomerImportExcelVO.builder().name("超级管理员").industryId(1).level(1).source(1)
                        .mobile("15601691300").telephone("").qq("").wechat("").email("yunai@iocoder.cn")
                        .areaId(null).detailAddress("").remark("").autoPoolEnabled(true).build(),
                CrmCustomerImportExcelVO.builder().name("源码").industryId(1).level(1).source(1)
                        .mobile("15601691300").telephone("").qq("").wechat("").email("yunai@iocoder.cn")
                        .areaId(null).detailAddress("").remark("").autoPoolEnabled(true).build()
                CrmCustomerImportExcelVO.builder().name("示例客户A(请替换)").industryId(1).level(1).source(1)
                        .mobile("13800000001").telephone("").qq("").wechat("").email("customerA@example.com")
                        .areaId(null).detailAddress("").remark("示例数据,导入前请替换").autoPoolEnabled(true).build(),
                CrmCustomerImportExcelVO.builder().name("示例客户B(请替换)").industryId(1).level(1).source(1)
                        .mobile("13800000002").telephone("").qq("").wechat("").email("customerB@example.com")
                        .areaId(null).detailAddress("").remark("示例数据,导入前请替换").autoPoolEnabled(true).build()
        );
        // è¾“出
        ExcelUtils.write(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java
@@ -157,7 +157,7 @@
                        .invoiceNo("02564178").invoiceTitle("示例客户有限公司").price(new BigDecimal("10000.00"))
                        .invoiceTime("2026-01-01 10:00:00").remark("示例数据,导入前请替换")
                        .build());
        ExcelUtils.write(response, "开票导入模板.xls", "开票", CrmInvoiceImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "开票导入模板.xls", "开票", CrmInvoiceImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java
@@ -182,7 +182,7 @@
                        .orderTime("2026-01-01 10:00:00").productCode("ITEM001").productName("示例产品A")
                        .count(new BigDecimal("100")).productPrice(new BigDecimal("10.00"))
                        .taxPercent(new BigDecimal("13")).qcCheckFlag("否").remark("示例").build());
        ExcelUtils.write(response, "采购订单导入模板.xls", "采购订单", ErpPurchaseOrderImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "采购订单导入模板.xls", "采购订单", ErpPurchaseOrderImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java
@@ -150,7 +150,7 @@
                        .count(new BigDecimal("100")).productPrice(new BigDecimal("10.00"))
                        .taxPercent(new BigDecimal("13")).demandTime("2026-01-10")
                        .qcCheckFlag("否").remark("示例").build());
        ExcelUtils.write(response, "采购申请导入模板.xls", "采购申请", ErpPurchaseRequestImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "采购申请导入模板.xls", "采购申请", ErpPurchaseRequestImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
@@ -220,7 +220,7 @@
                        .orderTime("2026-01-01 10:00:00").productCode("ITEM001").productName("示例产品A")
                        .count(new BigDecimal("100")).productPrice(new BigDecimal("20.00"))
                        .taxPercent(new BigDecimal("13")).needProduction("是").remark("示例").build());
        ExcelUtils.write(response, "销售订单导入模板.xls", "销售订单", ErpSaleOrderImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "销售订单导入模板.xls", "销售订单", ErpSaleOrderImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java
@@ -224,7 +224,7 @@
                        .remark("示例数据")
                        .build()
        );
        ExcelUtils.write(response, "考勤导入模板.xls", "考勤记录", HrmAttendanceRecordImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "考勤导入模板.xls", "考勤记录", HrmAttendanceRecordImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java
@@ -105,7 +105,7 @@
                        .probationSalary(new BigDecimal("8000.00")).regularSalary(new BigDecimal("10000.00"))
                        .remark("示例:请按实际修改后导入").build()
        );
        ExcelUtils.write(response, "员工合同导入模板.xls", "员工合同", HrmEmployeeContractImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "员工合同导入模板.xls", "员工合同", HrmEmployeeContractImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java
@@ -201,16 +201,28 @@
    @Operation(summary = "下载员工导入模板")
    @PreAuthorize("@ss.hasPermission('hrm:employee:import')")
    public void importTemplate(HttpServletResponse response) throws IOException {
        // è¾“出示例数据,方便使用者参考
        // ç¤ºä¾‹çš„部门/岗位/角色取当前系统真实数据,避免照抄 demo é‡Œçš„「研发部」导致匹配失败
        String sampleDept = "请填写部门名称";
        try {
            var depts = deptApi.getDeptList();
            if (depts != null && !depts.isEmpty()) {
                sampleDept = depts.get(0).getName();
            }
        } catch (Exception ignored) {
            // å–不到就保留占位文案,不影响模板下载
        }
        // å²—位未提供简易列表 API,示例用占位文案
        String samplePost = "请填写岗位名称(多个用英文逗号分隔)";
        List<HrmEmployeeImportExcelVO> list = Arrays.asList(
                HrmEmployeeImportExcelVO.builder().name("张三").gender(1).phone("13800138000")
                        .deptName("研发部").postName("部门经理,项目经理").roleNames("普通管理员")
                        .deptName(sampleDept).postName(samplePost).roleNames("普通角色")
                        .hireDate(LocalDate.of(2020, 3, 1)).employeeStatus(1)
                        .idCard("320102199001150011").birthday(LocalDate.of(1990, 1, 15))
                        .nation("汉族").marriageStatus("已婚").age(36).education("本科").politicalStatus("中共党员")
                        .email("zhangsan@example.com").build()
                        .email("zhangsan@example.com").remark("示例数据,导入前请替换").build()
        );
        ExcelUtils.write(response, "员工导入模板.xls", "员工列表", HrmEmployeeImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "员工导入模板.xls", "员工列表", HrmEmployeeImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java
@@ -17,6 +17,7 @@
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasurePageReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.enums.MdmItemTypeEnum;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
@@ -318,10 +319,38 @@
    @Operation(summary = "获得导入物料模板")
    @PreAuthorize("@ss.hasPermission('mdm:item:import')")
    public void importTemplate(HttpServletResponse response) throws IOException {
        // åˆ†ç±»ç¼–码、计量单位编码为必填项,示例取当前系统真实编码,避免照模板导入直接失败
        String sampleCategoryCode = "";
        try {
            List<MdmItemCategoryDO> cats = itemCategoryService.getSimpleItemList();
            if (cats != null && !cats.isEmpty()) {
                sampleCategoryCode = cats.get(0).getCode();
            }
        } catch (Exception ignored) {
            // å–不到留空,不影响模板下载
        }
        String sampleUnitCode = "";
        try {
            MdmUnitMeasurePageReqVO unitReq = new MdmUnitMeasurePageReqVO();
            unitReq.setPageNo(1);
            unitReq.setPageSize(1);
            List<MdmUnitMeasureDO> units = unitMeasureService.getUnitMeasurePage(unitReq).getList();
            if (units != null && !units.isEmpty()) {
                sampleUnitCode = units.get(0).getCode();
            }
        } catch (Exception ignored) {
            // åŒä¸Š
        }
        List<MdmItemImportExcelVO> list = Arrays.asList(
                MdmItemImportExcelVO.builder().code("ITEM001").name("示例物料").specification("规格").status(0).isBatchManaged(false).build()
                MdmItemImportExcelVO.builder()
                        .code("ITEM-EXAMPLE").name("示例物料(请替换)").specification("规格")
                        .categoryCode(sampleCategoryCode).unitMeasureCode(sampleUnitCode)
                        .status(0).isBatchManaged(false)
                        .remark("示例数据,导入前请替换;分类编码/单位编码请填写系统中已存在的编码")
                        .build()
        );
        ExcelUtils.write(response, "物料导入模板.xls", "物料", MdmItemImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "物料导入模板.xls", "物料", MdmItemImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/MdmUnitMeasureController.java
@@ -144,7 +144,7 @@
                MdmUnitMeasureImportExcelVO.builder().code("PC").name("片").primaryFlag(true).changeRate(new java.math.BigDecimal("1")).status(0).remark("示例:主单位").build(),
                MdmUnitMeasureImportExcelVO.builder().code("XIA").name("ç®±").primaryFlag(false).primaryCode("PC").changeRate(new java.math.BigDecimal("100")).status(0).remark("示例:1ç®±=100片").build()
        );
        ExcelUtils.write(response, "计量单位导入模板.xls", "计量单位", MdmUnitMeasureImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "计量单位导入模板.xls", "计量单位", MdmUnitMeasureImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/MesDvMachineryController.java
@@ -130,7 +130,7 @@
                        .workshopCode("WS-001").status(0).build()
        );
        // è¾“出
        ExcelUtils.write(response, "设备导入模板.xls", "设备列表", MesDvMachineryImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "设备导入模板.xls", "设备列表", MesDvMachineryImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/MesDvMeteringController.java
@@ -123,7 +123,7 @@
                        .deptName("研发部门").chargeUserName("超级管理员")
                        .checkCycleCount(12).useDate("2026-01-01").status(1).remark("示例数据,导入前请替换")
                        .build());
        ExcelUtils.write(response, "计量器具导入模板.xls", "计量器具", MesDvMeteringImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "计量器具导入模板.xls", "计量器具", MesDvMeteringImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/MesMdItemTypeController.java
@@ -153,7 +153,7 @@
                MesMdItemTypeImportExcelVO.builder().code("RAW").name("原材料").itemOrProduct("ITEM").sort(1).status(0).remark("示例:顶级分类").build(),
                MesMdItemTypeImportExcelVO.builder().code("METAL").name("金属材料").parentCode("RAW").itemOrProduct("ITEM").sort(1).status(0).remark("示例:父分类编码为 RAW").build()
        );
        ExcelUtils.write(response, "物料分类导入模板.xls", "物料分类", MesMdItemTypeImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "物料分类导入模板.xls", "物料分类", MesMdItemTypeImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/MesQcIpqcController.java
@@ -169,7 +169,7 @@
                        .checkResult("校验通过").inspectorName("").inspectDate("2026-01-01 10:00:00")
                        .remark("示例数据,导入前请替换")
                        .build());
        ExcelUtils.write(response, "过程检验单导入模板.xls", "过程检验", MesQcIpqcImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "过程检验单导入模板.xls", "过程检验", MesQcIpqcImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/iqc/MesQcIqcController.java
@@ -161,7 +161,7 @@
                        .inspectDate("2026-01-01 10:00:00").receiveDate("2026-01-01 09:00:00")
                        .remark("示例数据,导入前请替换")
                        .build());
        ExcelUtils.write(response, "来料检验单导入模板.xls", "来料检验", MesQcIqcImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "来料检验单导入模板.xls", "来料检验", MesQcIqcImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java
@@ -161,7 +161,7 @@
                        .checkResult("校验通过").inspectorName("").inspectDate("2026-01-01 10:00:00")
                        .outDate("2026-01-01 09:00:00").remark("示例数据,导入前请替换")
                        .build());
        ExcelUtils.write(response, "出货检验单导入模板.xls", "出货检验", MesQcOqcImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "出货检验单导入模板.xls", "出货检验", MesQcOqcImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java
@@ -148,7 +148,7 @@
                        .location("A区").area("A-01").batchCode("B20260101").vendor("XX供应商")
                        .operationType("覆盖").quantity(new BigDecimal("100")).build()
        );
        ExcelUtils.write(response, "库存现有量导入模板.xls", "库存列表",
        ExcelUtils.writeTemplate(response, "库存现有量导入模板.xls", "库存列表",
                MesWmMaterialStockImportExcelVO.class, list);
    }
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/supplier/SrmSupplierController.java
@@ -131,7 +131,7 @@
        sample.setBankAccount("6222020000000000000");
        sample.setStatus(0);
        sample.setRemark("导入模板示例行,可删除");
        ExcelUtils.write(response, "供应商导入模板.xlsx", "供应商", SrmSupplierImportExcelVO.class, Arrays.asList(sample));
        ExcelUtils.writeTemplate(response, "供应商导入模板.xlsx", "供应商", SrmSupplierImportExcelVO.class, Arrays.asList(sample));
    }
    @PostMapping("/import-excel")
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java
@@ -101,11 +101,21 @@
    @GetMapping("/get-import-template")
    @Operation(summary = "获得导入部门模板")
    public void importTemplate(HttpServletResponse response) throws IOException {
        // ç¤ºä¾‹çš„上级部门取当前真实根部门,避免照抄 demo é‡Œçš„「总公司」导致找不到上级
        List<DeptDO> allDepts = deptService.getDeptList(new DeptListReqVO());
        String rootPath = allDepts.stream()
                .filter(d -> d.getParentId() == null || d.getParentId() == 0)
                .map(DeptDO::getName)
                .findFirst()
                .orElse("请填写上级部门全路径");
        List<DeptImportExcelVO> list = Arrays.asList(
                DeptImportExcelVO.builder().name("研发部").parentPath("总公司").sort(1).status(CommonStatusEnum.ENABLE.getStatus()).build(),
                DeptImportExcelVO.builder().name("财务部").parentPath("总公司").sort(2).status(CommonStatusEnum.ENABLE.getStatus()).build()
                DeptImportExcelVO.builder().name("示例部门A").parentPath(rootPath).sort(1)
                        .status(CommonStatusEnum.ENABLE.getStatus()).build(),
                DeptImportExcelVO.builder().name("示例部门B").parentPath(rootPath).sort(2)
                        .status(CommonStatusEnum.ENABLE.getStatus()).build()
        );
        ExcelUtils.write(response, "部门导入模板.xls", "部门列表", DeptImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "部门导入模板.xls", "部门列表", DeptImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/PostController.java
@@ -127,7 +127,7 @@
                        .status(CommonStatusEnum.ENABLE.getStatus()).remark("前端开发工程师").build()
        );
        // è¾“出
        ExcelUtils.write(response, "岗位导入模板.xls", "岗位列表", PostImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "岗位导入模板.xls", "岗位列表", PostImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
@@ -240,7 +240,7 @@
                        .mobile("13800000002").nickname("李四")
                        .status(CommonStatusEnum.ENABLE.getStatus()).sex(SexEnum.FEMALE.getSex()).build()
        );
        ExcelUtils.write(response, "用户导入模板.xls", "用户列表", UserImportExcelVO.class, list);
        ExcelUtils.writeTemplate(response, "用户导入模板.xls", "用户列表", UserImportExcelVO.class, list);
    }
    @PostMapping("/import")
yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java
@@ -11,6 +11,7 @@
import cn.iocoder.yudao.module.infra.api.file.FileApi;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserImportExcelVO;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserImportRespVO;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserPageReqVO;
@@ -422,6 +423,7 @@
            o.setMobile(randomMobile());
        });
        // mock æ–¹æ³•,模拟失败
        when(deptService.getDeptList(any(DeptListReqVO.class))).thenReturn(newArrayList());
        doThrow(new ServiceException(DEPT_NOT_FOUND)).when(deptService).validateDeptList(any());
        // è°ƒç”¨
@@ -445,12 +447,12 @@
            o.setEmail(randomEmail());
            o.setMobile(randomMobile());
        });
        // mock deptService çš„æ–¹æ³•
        // mock deptService.getDeptList:新实现按「部门名称」匹配,不再按编号查单个部门
        DeptDO dept = randomPojo(DeptDO.class, o -> {
            o.setId(importUser.getDeptId());
            o.setStatus(CommonStatusEnum.ENABLE.getStatus());
        });
        when(deptService.getDept(eq(dept.getId()))).thenReturn(dept);
        importUser.setDeptName(dept.getName());
        when(deptService.getDeptList(any(DeptListReqVO.class))).thenReturn(newArrayList(dept));
        // mock passwordEncoder çš„æ–¹æ³•
        when(passwordEncoder.encode(eq("yudaoyuanma"))).thenReturn("java");
@@ -481,12 +483,12 @@
            o.setEmail(randomEmail());
            o.setMobile(randomMobile());
        });
        // mock deptService çš„æ–¹æ³•
        // mock deptService.getDeptList:新实现按「部门名称」匹配,不再按编号查单个部门
        DeptDO dept = randomPojo(DeptDO.class, o -> {
            o.setId(importUser.getDeptId());
            o.setStatus(CommonStatusEnum.ENABLE.getStatus());
        });
        when(deptService.getDept(eq(dept.getId()))).thenReturn(dept);
        importUser.setDeptName(dept.getName());
        when(deptService.getDeptList(any(DeptListReqVO.class))).thenReturn(newArrayList(dept));
        // è°ƒç”¨
        UserImportRespVO respVO = userService.importUserList(newArrayList(importUser), false);
@@ -513,12 +515,12 @@
            o.setEmail(randomEmail());
            o.setMobile(randomMobile());
        });
        // mock deptService çš„æ–¹æ³•
        // mock deptService.getDeptList:新实现按「部门名称」匹配,不再按编号查单个部门
        DeptDO dept = randomPojo(DeptDO.class, o -> {
            o.setId(importUser.getDeptId());
            o.setStatus(CommonStatusEnum.ENABLE.getStatus());
        });
        when(deptService.getDept(eq(dept.getId()))).thenReturn(dept);
        importUser.setDeptName(dept.getName());
        when(deptService.getDeptList(any(DeptListReqVO.class))).thenReturn(newArrayList(dept));
        // è°ƒç”¨
        UserImportRespVO respVO = userService.importUserList(newArrayList(importUser), true);