gaoluyang
2026-10-31 46855633ce1809df905efa8e0b03ca94a49a227b
feat(mes): 销售出库单支持 Excel 导入

- 新增导入模板下载与导入接口,权限 mes:wm-product-sales:import
- 按出库单号分组,同号多行合并为一张单,单号留空自动生成
- 每张单独立事务,失败整单回滚且不影响其它单
- 导入后固定草稿状态,不提交、不创建 OQC、不占用库存
- 失败原因精确到行号、字段与实际值(客户/物料/批次/数量/单号)

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加2个文件
已修改3个文件
410 ■■■■■ 文件已修改
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/MesWmProductSalesController.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/MesWmProductSalesImportExcelVO.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/MesWmProductSalesImportRespVO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java 259 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/MesWmProductSalesController.java
@@ -10,6 +10,8 @@
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesSaveReqVO;
@@ -27,8 +29,10 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.*;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
@@ -105,6 +109,36 @@
                buildRespVOList(pageResult.getList()));
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "获得销售出库单导入模板")
    @PreAuthorize("@ss.hasPermission('mes:wm-product-sales:import')")
    public void importTemplate(HttpServletResponse response) throws IOException {
        List<MesWmProductSalesImportExcelVO> list = Arrays.asList(
                // åŒä¸€å‡ºåº“单号的两行 = åŒä¸€å¼ å‡ºåº“单的两条明细
                MesWmProductSalesImportExcelVO.builder()
                        .code("PS202610310001").clientName("示例客户").name("示例客户-2026-10-31 å‡ºåº“")
                        .salesDate("2026-10-31").salesOrderCode("").contactName("张三")
                        .contactTelephone("13800138000").contactAddress("示例收货地址")
                        .itemCode("ITEM001").batchCode("").quantity(new BigDecimal("10"))
                        .oqcCheckFlag("否").remark("示例数据,导入前请替换").build(),
                MesWmProductSalesImportExcelVO.builder()
                        .code("PS202610310001").clientName("示例客户").name("示例客户-2026-10-31 å‡ºåº“")
                        .salesDate("2026-10-31").salesOrderCode("").contactName("张三")
                        .contactTelephone("13800138000").contactAddress("示例收货地址")
                        .itemCode("ITEM002").batchCode("").quantity(new BigDecimal("5"))
                        .oqcCheckFlag("否").remark("示例数据,导入前请替换").build());
        ExcelUtils.write(response, "销售出库单导入模板.xls", "销售出库单", MesWmProductSalesImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入销售出库单", description = "导入后生成草稿状态的出库单,不提交、不占用库存")
    @PreAuthorize("@ss.hasPermission('mes:wm-product-sales:import')")
    public CommonResult<MesWmProductSalesImportRespVO> importProductSalesExcel(
            @RequestParam("file") MultipartFile file) throws Exception {
        List<MesWmProductSalesImportExcelVO> list = ExcelUtils.read(file, MesWmProductSalesImportExcelVO.class);
        return success(productSalesService.importProductSalesList(list));
    }
    @PutMapping("/submit")
    @Operation(summary = "提交销售出库单")
    @Parameter(name = "id", description = "编号", required = true)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/MesWmProductSalesImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
 * ç®¡ç†åŽå° - é”€å”®å‡ºåº“单导入 Excel VO
 *
 * è¯´æ˜Žï¼šä¸€è¡Œ = ä¸€æ¡å‡ºåº“明细;相同“出库单号”的多行会合并为一张出库单(单头字段取该组首行)。
 * â€œå‡ºåº“单号”留空时,每一行各自生成一张出库单(单条明细),单号由系统自动生成。
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é”€å”®å‡ºåº“单导入 Excel VO")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MesWmProductSalesImportExcelVO {
    @ExcelProperty("出库单号")
    @Schema(description = "出库单号,留空则自动生成;相同单号的多行合并为同一张出库单")
    private String code;
    @ExcelProperty("客户名称")
    @Schema(description = "客户名称,需与 CRM ä¸­çš„客户名称完全一致", requiredMode = Schema.RequiredMode.REQUIRED)
    private String clientName;
    @ExcelProperty("出库单名称")
    @Schema(description = "出库单名称,留空默认“客户名称-出库日期 å‡ºåº“”;同一张单取首行")
    private String name;
    @ExcelProperty("出库日期")
    @Schema(description = "出库日期,格式 yyyy-MM-dd;留空取当天")
    private String salesDate;
    @ExcelProperty("销售订单号")
    @Schema(description = "销售订单号,仅作记录,可空")
    private String salesOrderCode;
    @ExcelProperty("收货人")
    @Schema(description = "收货人,可空")
    private String contactName;
    @ExcelProperty("联系方式")
    @Schema(description = "收货联系方式,可空")
    private String contactTelephone;
    @ExcelProperty("收货地址")
    @Schema(description = "收货地址,可空")
    private String contactAddress;
    @ExcelProperty("物料编码")
    @Schema(description = "物料编码,需与物料主数据中的编码一致", requiredMode = Schema.RequiredMode.REQUIRED)
    private String itemCode;
    @ExcelProperty("批次号")
    @Schema(description = "批次号,可空;填写时必须是系统中已存在的批次")
    private String batchCode;
    @ExcelProperty("出库数量")
    @Schema(description = "出库数量(物料主单位口径),必须大于 0", requiredMode = Schema.RequiredMode.REQUIRED)
    private BigDecimal quantity;
    @ExcelProperty("是否出厂检验")
    @Schema(description = "是否出厂检验:填“是/否”,留空默认“否”")
    private String oqcCheckFlag;
    @ExcelProperty("备注")
    @Schema(description = "备注;同一张单取首行备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/MesWmProductSalesImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
 * ç®¡ç†åŽå° - é”€å”®å‡ºåº“单导入 Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é”€å”®å‡ºåº“单导入 Response VO")
@Data
@Builder
public class MesWmProductSalesImportRespVO {
    @Schema(description = "创建成功的出库单号数组", example = "PS202610310001")
    private List<String> createList;
    @Schema(description = "导入失败集合,key ä¸ºå‡ºåº“单号或行号,value ä¸ºå¤±è´¥åŽŸå› ")
    private Map<String, String> failureList;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesService.java
@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.mes.service.wm.productsales;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesShippingReqVO;
@@ -132,5 +134,15 @@
     */
    void updateStatus(Long id, Integer status);
    /**
     * å¯¼å…¥é”€å”®å‡ºåº“单
     *
     * æ¯å¼ å‡ºåº“单单独事务:单张失败不影响其它单,失败的出库单不会残留半条数据。
     *
     * @param importList å¯¼å…¥æ•°æ®ï¼Œä¸€è¡Œ = ä¸€æ¡å‡ºåº“明细
     * @return å¯¼å…¥ç»“果(成功单号 + å¤±è´¥åŽŸå› ï¼‰
     */
    MesWmProductSalesImportRespVO importProductSalesList(List<MesWmProductSalesImportExcelVO> importList);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
@@ -2,13 +2,26 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesShippingReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.line.MesWmProductSalesLineSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDetailDO;
@@ -34,11 +47,20 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -79,6 +101,15 @@
    @Resource
    private ErpSaleOrderApi saleOrderApi;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private MesWmBatchService batchService;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private PlatformTransactionManager transactionManager;
    @Override
    public Long createProductSales(MesWmProductSalesSaveReqVO createReqVO) {
@@ -441,4 +472,232 @@
        productSalesMapper.updateById(new MesWmProductSalesDO().setId(id).setStatus(status));
    }
    // ==================== å¯¼å…¥ ====================
    @Override
    public MesWmProductSalesImportRespVO importProductSalesList(List<MesWmProductSalesImportExcelVO> importList) {
        MesWmProductSalesImportRespVO respVO = MesWmProductSalesImportRespVO.builder()
                .createList(new ArrayList<>())
                .failureList(new LinkedHashMap<>())
                .build();
        if (CollUtil.isEmpty(importList)) {
            return respVO;
        }
        // 1. é¢„热“客户名称 -> å®¢æˆ·ç¼–号”映射(客户主数据在 CRM æ¨¡å—)
        Map<String, Long> customerIdMap = new HashMap<>();
        List<CrmCustomerRespDTO> customers = customerApi.getCustomerSimpleList();
        if (CollUtil.isNotEmpty(customers)) {
            customers.forEach(customer -> {
                if (customer != null && StrUtil.isNotBlank(customer.getName())) {
                    customerIdMap.putIfAbsent(customer.getName().trim(), customer.getId());
                }
            });
        }
        // 2. æŒ‰â€œå‡ºåº“单号”分组:同单号多行 = ä¸€å¼ å‡ºåº“单的多条明细;单号为空则每行独立成单
        Map<String, List<ImportRow>> orderGroups = new LinkedHashMap<>();
        for (int i = 0; i < importList.size(); i++) {
            MesWmProductSalesImportExcelVO row = importList.get(i);
            if (row == null || isEmptyImportRow(row)) {
                continue; // è·³è¿‡ç©ºè¡Œ
            }
            String key = StrUtil.isNotBlank(row.getCode()) ? row.getCode().trim() : "__row_" + (i + 2);
            orderGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(new ImportRow(row, i + 2));
        }
        // 3. é€å•导入:每张单独立事务,失败的单整单回滚,不影响其它单
        TransactionTemplate importTxTemplate = new TransactionTemplate(transactionManager);
        for (Map.Entry<String, List<ImportRow>> entry : orderGroups.entrySet()) {
            List<ImportRow> rows = entry.getValue();
            String label = StrUtil.isNotBlank(rows.get(0).data().getCode())
                    ? rows.get(0).data().getCode().trim() : "第 " + rows.get(0).rowNum() + " è¡Œ";
            try {
                String code = importTxTemplate.execute(status -> createProductSalesByImport(rows, customerIdMap));
                respVO.getCreateList().add(code);
            } catch (Exception ex) {
                respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
            }
        }
        return respVO;
    }
    /**
     * ä¾æ®åŒä¸€å‡ºåº“单号的多行导入数据,创建一张销售出库单(单头 + æ˜Žç»†ï¼‰
     *
     * @return å‡ºåº“单号
     */
    private String createProductSalesByImport(List<ImportRow> rows, Map<String, Long> customerIdMap) {
        ImportRow headRow = rows.get(0);
        MesWmProductSalesImportExcelVO head = headRow.data();
        String rowPrefix = "第 " + headRow.rowNum() + " è¡Œï¼š";
        // 1. å®¢æˆ·ï¼ˆå–该组首行)
        if (StrUtil.isBlank(head.getClientName())) {
            throw new IllegalArgumentException(rowPrefix + "客户名称不能为空");
        }
        String clientName = head.getClientName().trim();
        Long clientId = customerIdMap.get(clientName);
        if (clientId == null) {
            throw new IllegalArgumentException(rowPrefix + "客户名称[" + clientName
                    + "]在 CRM ä¸­ä¸å­˜åœ¨ï¼Œè¯·æ ¸å¯¹åç§°æ˜¯å¦ä¸Žç³»ç»Ÿå®¢æˆ·å®Œå…¨ä¸€è‡´ï¼ˆå«å…¨åŠè§’、空格)");
        }
        // 2. å‡ºåº“日期(留空取当天)
        LocalDateTime salesDate = parseImportSalesDate(head.getSalesDate(), rowPrefix);
        // 3. å‡ºåº“单号:填了就用(校验唯一),没填自动生成
        String code = StrUtil.isNotBlank(head.getCode()) ? head.getCode().trim()
                : autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.WM_PRODUCT_SALES_CODE.getCode());
        if (StrUtil.isBlank(code)) {
            throw new IllegalArgumentException(rowPrefix
                    + "出库单号自动生成失败,请稍后重试,或在该列手工填写出库单号");
        }
        if (productSalesMapper.selectByCode(code) != null) {
            throw new IllegalArgumentException(rowPrefix + "出库单号[" + code
                    + "]已存在,请修改该列或清空该列由系统自动生成");
        }
        // 4. åˆ›å»ºå•头(草稿状态,不提交、不占用库存)
        MesWmProductSalesSaveReqVO saveReqVO = new MesWmProductSalesSaveReqVO();
        saveReqVO.setCode(code);
        saveReqVO.setName(StrUtil.isNotBlank(head.getName()) ? head.getName().trim()
                : clientName + "-" + salesDate.toLocalDate() + " å‡ºåº“");
        saveReqVO.setClientId(clientId);
        saveReqVO.setSalesDate(salesDate);
        saveReqVO.setSalesOrderCode(StrUtil.trimToNull(head.getSalesOrderCode()));
        saveReqVO.setContactName(StrUtil.trimToNull(head.getContactName()));
        saveReqVO.setContactTelephone(StrUtil.trimToNull(head.getContactTelephone()));
        saveReqVO.setContactAddress(StrUtil.trimToNull(head.getContactAddress()));
        saveReqVO.setRemark(StrUtil.trimToNull(head.getRemark()));
        Long salesId = createProductSales(saveReqVO);
        // 5. é€è¡Œåˆ›å»ºæ˜Žç»†
        for (ImportRow row : rows) {
            createProductSalesLineByImport(salesId, row);
        }
        return code;
    }
    /**
     * ä¾æ®å¯¼å…¥è¡Œåˆ›å»ºä¸€æ¡é”€å”®å‡ºåº“单行
     */
    private void createProductSalesLineByImport(Long salesId, ImportRow row) {
        MesWmProductSalesImportExcelVO data = row.data();
        String rowPrefix = "第 " + row.rowNum() + " è¡Œï¼š";
        // 1. ç‰©æ–™ç¼–码(必填,且必须存在、启用)
        if (StrUtil.isBlank(data.getItemCode())) {
            throw new IllegalArgumentException(rowPrefix + "物料编码不能为空");
        }
        String itemCode = data.getItemCode().trim();
        MdmItemRespDTO item = mdmItemApi.getItemByCode(itemCode).getCheckedData();
        if (item == null) {
            throw new IllegalArgumentException(rowPrefix + "物料编码[" + itemCode
                    + "]不存在,请先到【基础数据-物料主数据】中维护该物料");
        }
        if (!CommonStatusEnum.ENABLE.getStatus().equals(item.getStatus())) {
            throw new IllegalArgumentException(rowPrefix + "物料编码[" + itemCode
                    + "-" + item.getName() + "]已停用,请先在物料主数据中启用后再导入");
        }
        // 2. å‡ºåº“数量(必填,主单位口径)
        if (data.getQuantity() == null) {
            throw new IllegalArgumentException(rowPrefix + "出库数量不能为空(物料[" + itemCode + "])");
        }
        if (data.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException(rowPrefix + "出库数量必须大于 0,当前填写为["
                    + data.getQuantity().toPlainString() + "](物料[" + itemCode + "])");
        }
        // 3. æ‰¹æ¬¡å·ï¼ˆå¯ç©ºï¼›å¡«äº†å¿…须是已存在且归属该物料的批次)
        String batchCode = StrUtil.trimToNull(data.getBatchCode());
        if (batchCode != null) {
            MesWmBatchDO batch = batchService.getBatchByCode(batchCode);
            if (batch == null) {
                throw new IllegalArgumentException(rowPrefix + "批次号[" + batchCode
                        + "]不存在,请先在【批次管理】中创建,或清空该行的批次号");
            }
            if (batch.getItemId() != null && !ObjUtil.equal(batch.getItemId(), item.getId())) {
                String batchItemText = resolveItemText(batch.getItemId());
                throw new IllegalArgumentException(rowPrefix + "批次号[" + batchCode + "]属于物料["
                        + batchItemText + "],与当前行物料[" + itemCode + "-" + item.getName()
                        + "]不一致,请核对批次号或物料编码");
            }
        }
        // 4. åˆ›å»ºè¡Œï¼ˆè‰ç¨¿çŠ¶æ€ï¼›ç‰©æ–™å­˜åœ¨æ€§/启用、批次解析等校验由行服务统一完成)
        MesWmProductSalesLineSaveReqVO lineVO = new MesWmProductSalesLineSaveReqVO();
        lineVO.setSalesId(salesId);
        lineVO.setItemId(item.getId());
        lineVO.setQuantity(data.getQuantity());
        lineVO.setBatchCode(batchCode);
        lineVO.setOqcCheckFlag(parseImportYesNo(data.getOqcCheckFlag()));
        lineVO.setRemark(StrUtil.trimToNull(data.getRemark()));
        productSalesLineService.createProductSalesLine(lineVO);
    }
    private boolean isEmptyImportRow(MesWmProductSalesImportExcelVO row) {
        return StrUtil.isBlank(row.getCode()) && StrUtil.isBlank(row.getClientName())
                && StrUtil.isBlank(row.getItemCode()) && row.getQuantity() == null
                && StrUtil.isBlank(row.getName()) && StrUtil.isBlank(row.getSalesDate());
    }
    private LocalDateTime parseImportSalesDate(String text, String rowPrefix) {
        if (StrUtil.isBlank(text)) {
            return LocalDate.now().atStartOfDay();
        }
        String value = text.trim();
        for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm"}) {
            try {
                return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern));
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
            try {
                return LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        throw new IllegalArgumentException(rowPrefix + "出库日期[" + text
                + "]格式不正确,请使用 yyyy-MM-dd,如 2026-10-31");
    }
    private Boolean parseImportYesNo(String text) {
        if (StrUtil.isBlank(text)) {
            return false;
        }
        String value = text.trim();
        return "是".equals(value) || "1".equals(value) || "true".equalsIgnoreCase(value)
                || "yes".equalsIgnoreCase(value) || "需要".equals(value);
    }
    private String resolveItemText(Long itemId) {
        try {
            MdmItemRespDTO item = mdmItemApi.getItem(itemId).getCheckedData();
            if (item != null) {
                return item.getCode() + "-" + item.getName();
            }
        } catch (Exception ex) {
            log.warn("[resolveItemText][物料({}) åç§°è§£æžå¤±è´¥]", itemId, ex);
        }
        return "ID=" + itemId;
    }
    private String resolveImportErrorMessage(Exception ex) {
        if (ex instanceof ServiceException serviceException) {
            return serviceException.getMessage();
        }
        return StrUtil.blankToDefault(ex.getMessage(), "导入失败");
    }
    /**
     * å¯¼å…¥è¡Œï¼ˆä¿ç•™ Excel è¡Œå·ï¼Œä¾¿äºŽæç¤ºâ€œç¬¬ N è¡Œâ€ï¼‰
     */
    private record ImportRow(MesWmProductSalesImportExcelVO data, int rowNum) {
    }
}