From 2d1e4b5912355a42d51eca45f21be8648d7e08bf Mon Sep 17 00:00:00 2001 From: rcx <3084692334@qq.com> Date: Mon, 31 Aug 2026 11:15:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(oms):=E5=87=BA=E5=85=A5=E5=BA=93=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E6=8C=89=E7=BC=96=E5=8F=B7=E8=87=AA=E5=8A=A8=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=E5=90=8D=E7=A7=B0=E5=B9=B6=E6=A0=A1=E9=AA=8C=E5=AE=A2?= =?UTF-8?q?=E6=88=B7/=E7=89=A9=E6=96=99=E5=AD=98=E5=9C=A8=E6=80=A7?= =?UTF-8?q?=EF=BC=8C=E6=A8=A1=E6=9D=BF=E5=88=97=E4=BD=8D=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 需求:①入库业务单导入模板只填物料编号+客户编号,物料名称/客户名称按编号自动匹配库中数据 ②③出库导入校验客户/物料是否已存在,不存在提示"进行维护" ④库中查询结果为空(确无数据)时报错 一、入库导入(ReservationStockInOrderImpl.parseStockInExcel): 1. 物料逐编号查库,缺失料号收集后一次性报错"物料[xx]不存在,请先维护物料信息后再导入" 2. 明细新增客户编号:去重后查库,为空报错"客户[编号]不存在,请先维护客户信息后再导入"; 查到则以该客户为单据货主并自动匹配名称;与物料归属货主不一致时拦截并提示两者编号 3. 明细回填库中物料名称;未填客户编号时维持原"货主取自物料"逻辑,兼容老模板 二、出库导入(parseExcelFile): 1. 物料/客户校验前移到落库前(原为先落库后查客户,报错会留半截数据) 2. 物料查不到收集后报错维护提示;客户查不到报错维护提示,客户ID/名称改取库中匹配值 (替换仅返回id且查不到时静默存null的queryShipperId) 3. 產品編號/客户編号漏填的行直接报错提示补全 三、模板解析改为按列头文字动态定位列(繁简体归一化),列/行顺序调整不影响解析, 识别不到时回落老模板固定列位,老模板行为不变: 1. StockInExcelListener(入库):以產品編號为锚点识别列头行,新增客戶編號列支持 2. DynamicDeliveryExcelListener(出库):左右分栏各自锚点识别;送货日期/线号/车次/ 发票张数/小计/纪录合共改按标签文字定位,行号列号不再写死 四、Mapper:ReservationStockOutOrderMapper新增queryCustomerByCode(按组织查询, USER_MEMBER_CODE或USER_SHIPPER.CUSTOMER_NC_CODE任一命中),删除queryShipperId --- .../DynamicDeliveryExcelListener.java | 517 ++++++++++++------ .../listener/StockInExcelListener.java | 246 +++++++-- .../ReservationStockInOrderImpl.java | 184 +++++-- .../repository/vo/StockInHeader.java | 20 + .../repository/vo/StockInProductDetail.java | 14 +- .../ReservationStockOutOrderMapper.java | 8 +- .../ReservationStockOutOrderMapper.xml | 12 +- 7 files changed, 751 insertions(+), 250 deletions(-) diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/listener/DynamicDeliveryExcelListener.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/listener/DynamicDeliveryExcelListener.java index 9b898afb5..07ae79d33 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/listener/DynamicDeliveryExcelListener.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/listener/DynamicDeliveryExcelListener.java @@ -21,20 +21,47 @@ import java.util.Map; /** * 维他奶出库Excel解析监听器(新模板版) * 新模板特点:货品明细与客户配送明细左右分栏展示 - * 核心功能: - * 1. 左侧栏:第1-3行解析表头(送货日期/送货线号/车次),第5行解析纪录合共,第7行起解析货品明细 - * 2. 右侧栏:第2行解析发票张数/小计,第4行起解析客户配送明细 - * 3. 自动处理文本型数字转换(String→Integer/BigDecimal) - * 4. 完整的异常处理和日志记录 + * 核心功能(行列位置均不写死,模板调整行/列顺序不影响解析): + * 1. 表头标签(送货日期/送货线号/车次/发票张数/小计/纪录合共):按标签文字定位标签所在列,取右侧相邻列的值 + * 2. 货品明细:按列头文字(貨品編號/簡稱/箱/支等,繁简体归一化)定位各列,列头行之后的行按数据解析 + * 3. 客户配送明细:按列头文字(類别/發票編號/客户編號/名稱/箱/包/金額等)定位各列 + * 4. 列头行识别失败时兜底:按老模板固定布局(左栏A-D列、右栏F-L列、行号1-7)解析,保证老模板不受影响 + * 5. 自动处理文本型数字转换(String→Integer/BigDecimal) */ public class DynamicDeliveryExcelListener extends AnalysisEventListener> { // 日志实例 private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDeliveryExcelListener.class); - // 关键字定义(Excel标签匹配) - private static final String KEYWORD_TOTAL_RECORD = "紀錄合共"; - private static final String KEYWORD_INVOICE_COUNT = "發票張數"; - private static final String KEYWORD_SUBTOTAL = "小計"; + // ---------------------- 关键字/列头别名定义(归一化后匹配,兼容简繁体) ---------------------- + // 表头标签关键字 + private static final String[] KEYWORD_DELIVERY_DATE = {"送货日期"}; + private static final String[] KEYWORD_DELIVERY_LINE = {"送货线号", "送货线路"}; + private static final String[] KEYWORD_TRAIN = {"车次"}; + private static final String[] KEYWORD_TOTAL_RECORD = {"纪录合共"}; + private static final String[] KEYWORD_INVOICE_COUNT = {"发票张数"}; + private static final String[] KEYWORD_SUBTOTAL = {"小计"}; + + // 货品明细列头别名 + private static final String[] PRODUCT_CODE_ALIASES = {"货品编号", "产品编号", "物料编号", "货品编码", "产品编码", "物料编码", "料号"}; + private static final String[] PRODUCT_NAME_ALIASES = {"简称", "货品名称", "产品名称", "物料名称", "品名", "名称"}; + private static final String[] PRODUCT_BOX_ALIASES = {"箱数", "箱"}; + private static final String[] PRODUCT_PIECE_ALIASES = {"支数", "件数", "支"}; + + // 客户配送明细列头别名 + private static final String[] CATEGORY_ALIASES = {"类别"}; + private static final String[] INVOICE_NO_ALIASES = {"发票编号", "发票号", "发票"}; + private static final String[] CUSTOMER_CODE_ALIASES = {"客户编号", "客户编码", "客户代码"}; + private static final String[] CUSTOMER_NAME_ALIASES = {"客户名称", "名称"}; + private static final String[] CUSTOMER_BOX_ALIASES = {"箱数", "箱"}; + private static final String[] CUSTOMER_PACKAGE_ALIASES = {"包数", "包"}; + private static final String[] CUSTOMER_AMOUNT_ALIASES = {"金额", "总额"}; + + // 列头行认定门槛:锚点列(编号列)必须识别到,且识别列总数达标,防止数据行误判为列头行 + private static final int HEADER_MIN_MATCH_COUNT = 2; + // 老模板兜底:货品明细数据固定从第7行、客户配送明细数据固定从第4行开始(列头未识别到时启用固定列位) + private static final int PRODUCT_DATA_START_ROW = 7; + private static final int CUSTOMER_DATA_START_ROW = 4; + // 支持的日期格式(String转Date) private static final String[] SUPPORTED_DATE_FORMATS = {"yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "yyyy-MM-dd HH:mm:ss"}; @@ -43,6 +70,26 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener productDetailList = new ArrayList<>(); private final List customerDeliveryDetailList = new ArrayList<>(); + // 货品明细列索引(-1表示未识别该列) + private int productCodeCol = -1; + private int productNameCol = -1; + private int productBoxCol = -1; + private int productPieceCol = -1; + private boolean productColumnsResolved = false; + + // 客户配送明细列索引(-1表示未识别该列) + private int categoryCol = -1; + private int invoiceNoCol = -1; + private int customerCodeCol = -1; + private int customerNameCol = -1; + private int customerBoxCol = -1; + private int customerPackageCol = -1; + private int customerAmountCol = -1; + private boolean customerColumnsResolved = false; + + // 发票张数/小计行是否已解析 + private boolean invoiceRowParsed = false; + // 当前行号(1-based,与Excel行号一致) private int currentRowNum = 0; @@ -53,10 +100,10 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener data) { - switch (currentRowNum) { - case 1: - // 第1行:送货日期(A列=标签,B列=值) - parseDeliveryDate(data); - break; - case 2: - // 第2行:送货线号(A列=标签,B列=值) - parseDeliveryLineNo(data); - break; - case 3: - // 第3行:车次(A列=标签,B列=值) - parseTrainNumber(data); - break; - case 4: - // 第4行:货品明细标题行,无需解析 - LOGGER.debug("第{}行:货品明细标题行,跳过", currentRowNum); - break; - case 5: - // 第5行:纪录合共(A列=标签,B列=总记录数,D列=总箱数,E列=总支数) - parseTotalRecord(data); - break; - case 6: - // 第6行:货品明细列标题(貨品編號/簡稱/箱/支),跳过 - LOGGER.debug("第{}行:货品明细列标题行,跳过", currentRowNum); - break; - default: - // 第7行起:货品明细数据行(遇到空行自动结束) - if (currentRowNum >= 7 && isValidProductDetailRow(data)) { - parseProductDetail(data, currentRowNum); - } - break; + // 1. 表头标签:送货日期/送货线号/车次(按标签文字定位,行位置不限) + parseHeaderLabels(data); + // 2. 纪录合共(按标签文字定位,行位置不限) + parseTotalRecord(data); + // 3. 货品明细:列布局已确定时按数据行解析 + if (productColumnsResolved) { + if (isValidProductDetailRow(data)) { + parseProductDetail(data, currentRowNum); + } + return; + } + // 4. 尝试识别货品明细列头行;识别成功则本行即列头行,跳过数据解析 + if (tryResolveProductColumns(data)) { + return; + } + // 5. 超过老模板数据起始行仍未识别到列头行:启用固定列位兜底,从当前行开始按数据行解析 + if (currentRowNum >= PRODUCT_DATA_START_ROW) { + applyProductFallbackColumns(); + if (isValidProductDetailRow(data)) { + parseProductDetail(data, currentRowNum); + } } } // ---------------------- 右侧区域解析(客户配送相关信息) ---------------------- private void parseRightSection(Map data) { - switch (currentRowNum) { - case 1: - // 第1行:客户配送明细标题行,无需解析 - LOGGER.debug("第{}行:客户配送明细标题行,跳过", currentRowNum); - break; - case 2: - // 第2行:发票张数/小计(F列=發票張數标签,G列=数值;J列=小計标签,K/L/M列=数值) - parseInvoiceCount(data); - break; - case 3: - // 第3行:客户配送明细列标题(類别/發票編號/客户編號/名稱/箱/包/金額),跳过 - LOGGER.debug("第{}行:客户配送明细列标题行,跳过", currentRowNum); - break; - default: - // 第4行起:客户配送明细数据行(遇到空行自动结束) - if (currentRowNum >= 4 && isValidCustomerDetailRow(data)) { - parseCustomerDeliveryDetail(data, currentRowNum); - } - break; + // 1. 发票张数/小计(按标签文字定位,行位置不限,解析成功后不再重复解析) + if (!invoiceRowParsed) { + parseInvoiceRow(data); + } + // 2. 客户配送明细:列布局已确定时按数据行解析 + if (customerColumnsResolved) { + if (isValidCustomerDetailRow(data)) { + parseCustomerDeliveryDetail(data, currentRowNum); + } + return; + } + // 3. 尝试识别客户配送明细列头行;识别成功则本行即列头行,跳过数据解析 + if (tryResolveCustomerColumns(data)) { + return; + } + // 4. 超过老模板数据起始行仍未识别到列头行:启用固定列位兜底,从当前行开始按数据行解析 + if (currentRowNum >= CUSTOMER_DATA_START_ROW) { + applyCustomerFallbackColumns(); + if (isValidCustomerDetailRow(data)) { + parseCustomerDeliveryDetail(data, currentRowNum); + } } } // ---------------------- 解析完成回调 ---------------------- @Override public void doAfterAllAnalysed(AnalysisContext context) { + // 小计标签始终未识别到时保持与原逻辑一致:小计相关字段置0 + if (deliveryHeader.getSubtotalAmount() == null) { + LOGGER.warn("未找到【小计】标签,小计信息置空"); + deliveryHeader.setSubtotalBoxCount(0); + deliveryHeader.setSubtotalPieceCount(0); + deliveryHeader.setSubtotalAmount(BigDecimal.ZERO); + } LOGGER.info("Excel解析全部完成!最终统计结果:"); LOGGER.info("1. 表头信息:送货日期={},送货线号={},车次={},发票张数={},总记录数={},小计金额={}", deliveryHeader.getDeliveryDate(), @@ -144,47 +190,52 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener data) { - Object value = getValueSafely(data, 1); // B列 - if (value != null) { - Date deliveryDate = convertToDate(value); - deliveryHeader.setDeliveryDate(deliveryDate); - LOGGER.debug("第{}行:送货日期解析完成,值={}", currentRowNum, deliveryDate); + // ---------------------- 左侧:表头信息解析(标签定位,行位置不限) ---------------------- + private void parseHeaderLabels(Map data) { + // 送货日期 + if (deliveryHeader.getDeliveryDate() == null) { + Integer keywordIndex = findKeywordIndex(data, KEYWORD_DELIVERY_DATE); + if (keywordIndex != null) { + Date deliveryDate = convertToDate(getValueSafely(data, keywordIndex + 1)); + if (deliveryDate != null) { + deliveryHeader.setDeliveryDate(deliveryDate); + LOGGER.debug("第{}行:送货日期解析完成,值={}", currentRowNum, deliveryDate); + } + } + } + // 送货线号 + if (deliveryHeader.getDeliveryLineNo() == null || deliveryHeader.getDeliveryLineNo().trim().isEmpty()) { + Integer keywordIndex = findKeywordIndex(data, KEYWORD_DELIVERY_LINE); + if (keywordIndex != null) { + Object value = getValueSafely(data, keywordIndex + 1); + if (value != null && !value.toString().trim().isEmpty()) { + deliveryHeader.setDeliveryLineNo(value.toString().trim()); + LOGGER.debug("第{}行:送货线号解析完成,值={}", currentRowNum, deliveryHeader.getDeliveryLineNo()); + } + } + } + // 车次 + if (deliveryHeader.getTrainNumber() == null) { + Integer keywordIndex = findKeywordIndex(data, KEYWORD_TRAIN); + if (keywordIndex != null) { + deliveryHeader.setTrainNumber(convertToInteger(getValueSafely(data, keywordIndex + 1))); + LOGGER.debug("第{}行:车次解析完成,值={}", currentRowNum, deliveryHeader.getTrainNumber()); + } } } - private void parseDeliveryLineNo(Map data) { - Object value = getValueSafely(data, 1); // B列 - if (value != null) { - String deliveryLineNo = value.toString().trim(); - deliveryHeader.setDeliveryLineNo(deliveryLineNo); - LOGGER.debug("第{}行:送货线号解析完成,值={}", currentRowNum, deliveryLineNo); - } - } - - private void parseTrainNumber(Map data) { - Object value = getValueSafely(data, 1); // B列 - Integer trainNumber = convertToInteger(value); - deliveryHeader.setTrainNumber(trainNumber); - LOGGER.debug("第{}行:车次解析完成,值={}", currentRowNum, trainNumber); - } - - // ---------------------- 左侧:纪录合共解析 ---------------------- + // ---------------------- 左侧:纪录合共解析(标签定位,行位置不限) ---------------------- /** - * 第5行:纪录合共 - * 列对应关系:0=A列=紀錄合共标签,1=B列=总记录数,3=D列=总箱数,4=E列=总支数 + * 纪录合共:标签右侧第1列=总记录数,第3列=总箱数,第4列=总支数(与老模板B/D/E列偏移一致) */ private void parseTotalRecord(Map data) { - Object totalRecordLabel = getValueSafely(data, 0); - if (totalRecordLabel == null || !totalRecordLabel.toString().trim().contains(KEYWORD_TOTAL_RECORD)) { - LOGGER.warn("第{}行:不是纪录合共行,跳过解析,第一列内容:{}", currentRowNum, totalRecordLabel); + Integer keywordIndex = findKeywordIndex(data, KEYWORD_TOTAL_RECORD); + if (keywordIndex == null) { return; } - - Integer totalRecordCount = convertToInteger(getValueSafely(data, 1)); // B列 - Integer totalBoxCount = convertToInteger(getValueSafely(data, 3)); // D列 - Integer totalPieceCount = convertToInteger(getValueSafely(data, 4)); // E列 + Integer totalRecordCount = convertToInteger(getValueSafely(data, keywordIndex + 1)); + Integer totalBoxCount = convertToInteger(getValueSafely(data, keywordIndex + 3)); + Integer totalPieceCount = convertToInteger(getValueSafely(data, keywordIndex + 4)); deliveryHeader.setTotalRecordCount(totalRecordCount); deliveryHeader.setTotalBoxCount(totalBoxCount); @@ -194,27 +245,66 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener data) { - Object productCode = getValueSafely(data, 0); // A列 - return productCode != null - && !productCode.toString().trim().isEmpty() - && !"貨品編號".equals(productCode.toString().trim()); + private boolean tryResolveProductColumns(Map data) { + int codeCol = findColumnByAliases(data, PRODUCT_CODE_ALIASES); + if (codeCol < 0) { + return false; + } + int nameCol = findColumnByAliases(data, PRODUCT_NAME_ALIASES); + int boxCol = findColumnByAliases(data, PRODUCT_BOX_ALIASES); + int pieceCol = findColumnByAliases(data, PRODUCT_PIECE_ALIASES); + int matched = 1 + (nameCol >= 0 ? 1 : 0) + (boxCol >= 0 ? 1 : 0) + (pieceCol >= 0 ? 1 : 0); + if (matched < HEADER_MIN_MATCH_COUNT) { + return false; + } + productCodeCol = codeCol; + productNameCol = nameCol; + productBoxCol = boxCol; + productPieceCol = pieceCol; + productColumnsResolved = true; + LOGGER.info("第{}行识别为货品明细列头行,列映射:货品编号={},名称={},箱={},支={}", + currentRowNum, productCodeCol, productNameCol, productBoxCol, productPieceCol); + return true; } /** - * 解析货品明细(第7行起) - * 列对应关系:0=A列=貨品編號,1=B列=簡稱,2=C列=箱,3=D列=支 + * 兜底列位:老模板货品明细固定布局(0=A列=貨品編號,1=B列=簡稱,2=C列=箱,3=D列=支) + */ + private void applyProductFallbackColumns() { + productCodeCol = 0; + productNameCol = 1; + productBoxCol = 2; + productPieceCol = 3; + productColumnsResolved = true; + LOGGER.warn("第{}行前未识别到货品明细列头行,启用老模板固定列位兜底解析", currentRowNum); + } + + /** + * 校验货品明细行是否有效(排除列头行和空行) + */ + private boolean isValidProductDetailRow(Map data) { + Object productCode = getValueSafely(data, productCodeCol); + if (productCode == null || productCode.toString().trim().isEmpty()) { + return false; + } + // 列头文字本身不算数据行(防止列头行被当作数据重复解析) + String text = normalizeHeaderText(productCode); + return !matchesAny(text, PRODUCT_CODE_ALIASES); + } + + /** + * 解析货品明细(列头行之后的行) */ private void parseProductDetail(Map data, int rowNum) { ProductDetail productDetail = new ProductDetail(); - productDetail.setProductCode(convertToString(getValueSafely(data, 0))); - productDetail.setProductName(convertToString(getValueSafely(data, 1))); - productDetail.setBoxCount(convertToInteger(getValueSafely(data, 2))); - productDetail.setPieceCount(convertToInteger(getValueSafely(data, 3))); + productDetail.setProductCode(convertToString(getValueSafely(data, productCodeCol))); + productDetail.setProductName(convertToString(getValueSafely(data, productNameCol))); + productDetail.setBoxCount(convertToInteger(getValueSafely(data, productBoxCol))); + productDetail.setPieceCount(convertToInteger(getValueSafely(data, productPieceCol))); productDetailList.add(productDetail); LOGGER.debug("第{}行:解析货品明细完成,货品编号={},名称={},箱数={},支数={}", @@ -222,67 +312,105 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener data) { - // 1. 定位并解析发票张数 - Integer invoiceCount = findValueAfterKeyword(data, KEYWORD_INVOICE_COUNT); - if (invoiceCount != null) { - deliveryHeader.setInvoiceCount(invoiceCount); + // ---------------------- 右侧:发票张数与小计解析(标签定位,行位置不限) ---------------------- + private void parseInvoiceRow(Map data) { + // 1. 发票张数:标签右侧相邻列 + Integer invoiceIndex = findKeywordIndex(data, KEYWORD_INVOICE_COUNT); + if (invoiceIndex != null) { + deliveryHeader.setInvoiceCount(convertToInteger(getValueSafely(data, invoiceIndex + 1))); } - // 2. 定位并解析小计(标签右侧第1列=箱,第2列=支,第3列=金额) - Integer subtotalStartIndex = findKeywordIndex(data, KEYWORD_SUBTOTAL); - if (subtotalStartIndex != null && subtotalStartIndex >= 0) { - Integer subtotalBoxCount = convertToInteger(getValueSafely(data, subtotalStartIndex + 1)); - Integer subtotalPieceCount = convertToInteger(getValueSafely(data, subtotalStartIndex + 2)); - BigDecimal subtotalAmount = convertToBigDecimal(getValueSafely(data, subtotalStartIndex + 3)); + // 2. 小计:标签右侧第1列=箱,第2列=支,第3列=金额 + Integer subtotalIndex = findKeywordIndex(data, KEYWORD_SUBTOTAL); + if (subtotalIndex != null && subtotalIndex >= 0) { + Integer subtotalBoxCount = convertToInteger(getValueSafely(data, subtotalIndex + 1)); + Integer subtotalPieceCount = convertToInteger(getValueSafely(data, subtotalIndex + 2)); + BigDecimal subtotalAmount = convertToBigDecimal(getValueSafely(data, subtotalIndex + 3)); deliveryHeader.setSubtotalBoxCount(subtotalBoxCount); deliveryHeader.setSubtotalPieceCount(subtotalPieceCount); deliveryHeader.setSubtotalAmount(subtotalAmount); + invoiceRowParsed = true; - LOGGER.debug("第{}行:解析小计完成,箱数={},支数={},金额={}", - currentRowNum, subtotalBoxCount, subtotalPieceCount, subtotalAmount); - } else { - LOGGER.warn("第{}行:未找到小计标签,小计信息置空", currentRowNum); - deliveryHeader.setSubtotalBoxCount(0); - deliveryHeader.setSubtotalPieceCount(0); - deliveryHeader.setSubtotalAmount(BigDecimal.ZERO); + LOGGER.debug("第{}行:解析发票张数行完成,发票张数={},小计箱数={},支数={},金额={}", + currentRowNum, deliveryHeader.getInvoiceCount(), subtotalBoxCount, subtotalPieceCount, subtotalAmount); } - - LOGGER.debug("第{}行:解析发票张数行完成,发票张数={},小计金额={}", - currentRowNum, deliveryHeader.getInvoiceCount(), deliveryHeader.getSubtotalAmount()); } - // ---------------------- 右侧:客户配送明细解析 ---------------------- + // ---------------------- 右侧:客户配送明细列头识别与数据解析 ---------------------- /** - * 校验客户配送明细行是否有效(排除表头和空行) + * 尝试将当前行识别为客户配送明细列头行:以客户编号列为锚点,按列头文字(简繁归一化)匹配各列 + */ + private boolean tryResolveCustomerColumns(Map data) { + int codeCol = findColumnByAliases(data, CUSTOMER_CODE_ALIASES); + if (codeCol < 0) { + return false; + } + int categoryColTmp = findColumnByAliases(data, CATEGORY_ALIASES); + int invoiceNoColTmp = findColumnByAliases(data, INVOICE_NO_ALIASES); + int nameColTmp = findColumnByAliases(data, CUSTOMER_NAME_ALIASES); + int boxColTmp = findColumnByAliases(data, CUSTOMER_BOX_ALIASES); + int packageColTmp = findColumnByAliases(data, CUSTOMER_PACKAGE_ALIASES); + int amountColTmp = findColumnByAliases(data, CUSTOMER_AMOUNT_ALIASES); + int matched = 1 + (categoryColTmp >= 0 ? 1 : 0) + (invoiceNoColTmp >= 0 ? 1 : 0) + (nameColTmp >= 0 ? 1 : 0) + + (boxColTmp >= 0 ? 1 : 0) + (packageColTmp >= 0 ? 1 : 0) + (amountColTmp >= 0 ? 1 : 0); + if (matched < HEADER_MIN_MATCH_COUNT) { + return false; + } + categoryCol = categoryColTmp; + invoiceNoCol = invoiceNoColTmp; + customerCodeCol = codeCol; + customerNameCol = nameColTmp; + customerBoxCol = boxColTmp; + customerPackageCol = packageColTmp; + customerAmountCol = amountColTmp; + customerColumnsResolved = true; + LOGGER.info("第{}行识别为客户配送明细列头行,列映射:类别={},发票编号={},客户编号={},名称={},箱={},包={},金额={}", + currentRowNum, categoryCol, invoiceNoCol, customerCodeCol, customerNameCol, customerBoxCol, customerPackageCol, customerAmountCol); + return true; + } + + /** + * 兜底列位:老模板客户配送明细固定布局(5=F列=類别,6=G列=發票編號,7=H列=客户編號,8=I列=名稱, + * 9=J列=箱,10=K列=包,11=L列=金額) + */ + private void applyCustomerFallbackColumns() { + categoryCol = 5; + invoiceNoCol = 6; + customerCodeCol = 7; + customerNameCol = 8; + customerBoxCol = 9; + customerPackageCol = 10; + customerAmountCol = 11; + customerColumnsResolved = true; + LOGGER.warn("第{}行前未识别到客户配送明细列头行,启用老模板固定列位兜底解析", currentRowNum); + } + + /** + * 校验客户配送明细行是否有效(排除列头行和空行) */ private boolean isValidCustomerDetailRow(Map data) { - Object category = getValueSafely(data, 5); // F列 - return category != null - && !category.toString().trim().isEmpty() - && !"類别".equals(category.toString().trim()); + Object category = getValueSafely(data, categoryCol); + if (category == null || category.toString().trim().isEmpty()) { + return false; + } + // 列头文字本身不算数据行(防止列头行被当作数据重复解析) + String text = normalizeHeaderText(category); + return !matchesAny(text, CATEGORY_ALIASES); } /** - * 解析客户配送明细(第4行起) - * 列对应关系:5=F列=類别,6=G列=發票編號,7=H列=客户編號,8=I列=名稱, - * 9=J列=箱,10=K列=包,11=L列=金額 + * 解析客户配送明细(列头行之后的行) */ private void parseCustomerDeliveryDetail(Map data, int rowNum) { CustomerDeliveryDetail customerDetail = new CustomerDeliveryDetail(); - customerDetail.setCategory(convertToString(getValueSafely(data, 5))); - customerDetail.setInvoiceNo(convertToString(getValueSafely(data, 6))); - customerDetail.setCustomerCode(convertToString(getValueSafely(data, 7))); - customerDetail.setCustomerName(convertToString(getValueSafely(data, 8))); - customerDetail.setBoxCount(convertToInteger(getValueSafely(data, 9))); - customerDetail.setPackageCount(convertToInteger(getValueSafely(data, 10))); - customerDetail.setAmount(convertToBigDecimal(getValueSafely(data, 11))); + customerDetail.setCategory(convertToString(getValueSafely(data, categoryCol))); + customerDetail.setInvoiceNo(convertToString(getValueSafely(data, invoiceNoCol))); + customerDetail.setCustomerCode(convertToString(getValueSafely(data, customerCodeCol))); + customerDetail.setCustomerName(convertToString(getValueSafely(data, customerNameCol))); + customerDetail.setBoxCount(convertToInteger(getValueSafely(data, customerBoxCol))); + customerDetail.setPackageCount(convertToInteger(getValueSafely(data, customerPackageCol))); + customerDetail.setAmount(convertToBigDecimal(getValueSafely(data, customerAmountCol))); customerDeliveryDetailList.add(customerDetail); LOGGER.debug("第{}行:解析客户配送明细完成,类别={},发票编号={},客户编号={},名称={},箱数={},包数={},金额={}", @@ -299,7 +427,7 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener data, int columnIndex) { - if (data == null || data.isEmpty()) { + if (columnIndex < 0 || data == null || data.isEmpty()) { return null; } @@ -333,9 +461,9 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener data, String keyword) { - if (data == null || data.isEmpty() || keyword == null) { + private Integer findKeywordIndex(Map data, String[] keywords) { + if (data == null || data.isEmpty() || keywords == null) { return null; } for (Object keyObj : data.keySet()) { Object value = data.get(keyObj); - if (value != null && value.toString().trim().contains(keyword)) { + if (value == null) { + continue; + } + String text = normalizeHeaderText(value); + if (text.isEmpty()) { + continue; + } + if (matchesAny(text, keywords)) { try { return Integer.parseInt(keyObj.toString()); } catch (NumberFormatException e) { - // key不是数字(可能是表头文本如"客户配送明细"),跳过,不打印警告避免日志刷屏 - LOGGER.debug("第{}行:关键字'{}'所在列key不是数字,key={},跳过", - currentRowNum, keyword, keyObj); + // key不是数字(可能是表头文本),跳过,不打印警告避免日志刷屏 + LOGGER.debug("第{}行:关键字所在列key不是数字,key={},跳过", currentRowNum, keyObj); } } } @@ -366,15 +500,64 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener data, String keyword) { - Integer keywordIndex = findKeywordIndex(data, keyword); - if (keywordIndex == null) { - LOGGER.warn("第{}行:未找到关键字'{}'", currentRowNum, keyword); - return null; + private int findColumnByAliases(Map data, String[] aliases) { + if (data == null || data.isEmpty() || aliases == null) { + return -1; } - return convertToInteger(getValueSafely(data, keywordIndex + 1)); + for (Object keyObj : data.keySet()) { + Object value = data.get(keyObj); + if (value == null) { + continue; + } + String text = normalizeHeaderText(value); + if (text.isEmpty()) { + continue; + } + if (matchesAny(text, aliases)) { + try { + return Integer.parseInt(keyObj.toString()); + } catch (NumberFormatException e) { + // key不是数字(可能是表头文本),跳过 + LOGGER.debug("第{}行:列头所在列key不是数字,key={},跳过", currentRowNum, keyObj); + } + } + } + return -1; + } + + /** + * 列头/标签文字归一化:去空格/冒号/括号等符号、转小写、繁体转简体(覆盖模板常见用字) + */ + private String normalizeHeaderText(Object value) { + if (value == null) { + return ""; + } + String s = value.toString().trim().toLowerCase(); + s = s.replace(":", "").replace(":", "") + .replace(" ", "").replace(" ", "") + .replace("(", "").replace(")", "") + .replace("(", "").replace(")", "") + .replace("/", "").replace("\\", ""); + s = s.replace('貨', '货').replace('編', '编').replace('號', '号').replace('稱', '称') + .replace('數', '数').replace('飲', '饮').replace('規', '规').replace('戶', '户') + .replace('紀', '纪').replace('錄', '录').replace('張', '张').replace('計', '计') + .replace('車', '车').replace('線', '线').replace('類', '类').replace('別', '别') + .replace('簡', '简').replace('產', '产').replace('單', '单'); + return s; + } + + /** + * 判断归一化后的文字是否命中任一别名 + */ + private boolean matchesAny(String text, String[] aliases) { + for (String alias : aliases) { + if (text.contains(alias)) { + return true; + } + } + return false; } /** diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/listener/StockInExcelListener.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/listener/StockInExcelListener.java index c7df783b9..f8493da8c 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/listener/StockInExcelListener.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/listener/StockInExcelListener.java @@ -17,7 +17,11 @@ import java.util.Map; /** * 入库单Excel解析监听器 - * 功能:1. 解析第一行表头数据 2. 跳过第二行合计行、第三行列名行 3. 解析第四行开始的产品明细 4. 支持文本型数字转换 + * 功能:1. 解析第一行表头数据(入库日期、OrderNO,按标签定位,位置不敏感) + * 2. 动态定位列头行:扫描前几行,识别出"產品編號"等已知列头文字的行即认定为列头行(行位置不敏感) + * 3. 列按列头文字动态定位(兼容繁体列头:產品編號/客戶編號/件數/飲用日期等),列顺序调整不影响解析 + * 4. 列头无法识别时兜底:按老模板固定列位(0=编号,1=名称,2=规格,3=件数,4=饮用日期) + * 5. 支持文本型数字转换 */ public class StockInExcelListener extends AnalysisEventListener> { private static final Logger LOGGER = LoggerFactory.getLogger(StockInExcelListener.class); @@ -35,6 +39,29 @@ public class StockInExcelListener extends AnalysisEventListener= 4) { - if (isValidProductRow(data)) { - StockInProductDetail detail = parseProductDetail(data); - productDetailList.add(detail); + // 2. 列布局未确定:尝试识别列头行;识别成功则本行即为列头行,跳过数据解析 + else if (!columnResolved) { + if (!tryResolveHeaderRow(data)) { + // 3. 超过老模板列头行位置仍未识别到列头行:启用固定列位兜底,从当前行开始按数据行解析 + if (currentRowNum > FALLBACK_AFTER_ROW_NUM) { + applyFallbackColumns(); + parseDataRow(data); + } + // 否则(合计行Total等):跳过 } } + // 4. 列布局已确定:按数据行解析 + else { + parseDataRow(data); + } } catch (ExcelDataConvertException e) { // Excel内置转换异常(如日期格式错误) @@ -79,6 +106,16 @@ public class StockInExcelListener extends AnalysisEventListener data) { + if (isValidProductRow(data)) { + StockInProductDetail detail = parseProductDetail(data); + productDetailList.add(detail); + } + } + /** * 解析第一行表头数据 * 模板中标签与值相邻,分别搜索"入库日期"和"OrderNO"标签后一列的值,兼容位置调整 @@ -86,15 +123,17 @@ public class StockInExcelListener extends AnalysisEventListener data) { // 入库日期:查找包含"入库日期"标签的下一个值 Object stockInDateObj = getValueAfterLabel(data, "入庫日期:"); + if (stockInDateObj == null) { + stockInDateObj = getValueAfterLabel(data, "入库日期"); + } if (stockInDateObj != null) { Date stockInDate = convertToDate(stockInDateObj); stockInHeader.setStockInDate(stockInDate); LOGGER.debug("第1行:入库日期解析完成,值:{}", stockInDate); } - // OrderNO:优先查找"OrderNO"标签后的值(老模板布局,标签与值相邻); - // 新模板第1行无"OrderNO"标签,单号值直接放在单元格中(如E1), - // 此时取第1行中除标签、入库日期以外的第一个非空字符串值 + // OrderNO:优先查找"OrderNO"标签后的值(标签与值相邻); + // 新模板第1行无"OrderNO"标签时,取第1行中除标签、入库日期以外的第一个非空字符串值 Object orderNoObj = getValueAfterLabel(data, "OrderNO"); if (orderNoObj == null) { for (Object value : data.values()) { @@ -103,7 +142,7 @@ public class StockInExcelListener extends AnalysisEventListener data) { + if (data == null || data.isEmpty()) { + return false; + } + int tmpProductCodeCol = -1; + int tmpProductNameCol = -1; + int tmpCustomerCodeCol = -1; + int tmpDrinkDateCol = -1; + int tmpPieceCountCol = -1; + int tmpSpecCol = -1; + int matched = 0; + + for (Object keyObj : data.keySet()) { + Object value = data.get(keyObj); + if (value == null) { + continue; + } + String text = normalizeHeaderText(value); + if (text.isEmpty()) { + continue; + } + int colIndex; + try { + colIndex = Integer.parseInt(keyObj.toString()); + } catch (NumberFormatException e) { + // key不是数字(如表头文本),跳过 + continue; + } + // 按组依次匹配(先具体后一般,避免同一列头命中多组别名时归属错乱) + if (tmpProductCodeCol < 0 && matchesAny(text, PRODUCT_CODE_ALIASES)) { + tmpProductCodeCol = colIndex; + matched++; + } else if (tmpCustomerCodeCol < 0 && matchesAny(text, CUSTOMER_CODE_ALIASES)) { + tmpCustomerCodeCol = colIndex; + matched++; + } else if (tmpProductNameCol < 0 && matchesAny(text, PRODUCT_NAME_ALIASES)) { + tmpProductNameCol = colIndex; + matched++; + } else if (tmpDrinkDateCol < 0 && matchesAny(text, DRINK_DATE_ALIASES)) { + tmpDrinkDateCol = colIndex; + matched++; + } else if (tmpPieceCountCol < 0 && matchesAny(text, PIECE_COUNT_ALIASES)) { + tmpPieceCountCol = colIndex; + matched++; + } else if (tmpSpecCol < 0 && matchesAny(text, SPEC_ALIASES)) { + tmpSpecCol = colIndex; + matched++; + } + } + + // 产品编号列是数据解析的锚点,必须识别到;且识别总数达标才认定为列头行 + if (tmpProductCodeCol < 0 || matched < HEADER_MIN_MATCH_COUNT) { + return false; + } + + productCodeCol = tmpProductCodeCol; + productNameCol = tmpProductNameCol; + customerCodeCol = tmpCustomerCodeCol; + drinkDateCol = tmpDrinkDateCol; + pieceCountCol = tmpPieceCountCol; + specCol = tmpSpecCol; + columnResolved = true; + LOGGER.info("第{}行识别为列头行,列映射:产品编号={},产品名称={},客户编号={},规格={},件数={},饮用日期={}", + currentRowNum, productCodeCol, productNameCol, customerCodeCol, specCol, pieceCountCol, drinkDateCol); + return true; + } + + /** + * 兜底列位:老模板固定布局(0=编号,1=名称,2=规格,3=件数,4=饮用日期),无客户编号列 + */ + private void applyFallbackColumns() { + productCodeCol = 0; + productNameCol = 1; + customerCodeCol = -1; + specCol = 2; + pieceCountCol = 3; + drinkDateCol = 4; + columnResolved = true; + LOGGER.warn("第{}行前未识别到列头行,启用老模板固定列位兜底解析", currentRowNum); + } + /** * 解析产品明细数据(一行对应一个产品) - * 新模板列顺序:0=产品编号,1=产品名称,2=规格(箱含量),3=件数,4=饮用日期 + * 各列按列头行动态定位的索引取值,未识别的列取null/默认值 */ private StockInProductDetail parseProductDetail(Map data) { StockInProductDetail detail = new StockInProductDetail(); - // 产品编号(第1列,索引0)- 支持文本型数字转换 - detail.setProductCode(convertToString(getValueSafely(data, 0))); - // 产品名称(第2列,索引1) - detail.setProductName(convertToString(getValueSafely(data, 1))); - // 箱含量/规格(第3列,索引2) - detail.setBoxCapacity(convertToInteger(getValueSafely(data, 2))); - // 件数(第4列,索引3) - detail.setPieceCount(convertToInteger(getValueSafely(data, 3))); - // 饮用日期(第5列,索引4) - detail.setDrinkDate(convertToDate(getValueSafely(data, 4))); + // 产品编号(可不填名称,导入时按编号自动匹配库中物料名称) + detail.setProductCode(convertToString(getValueSafely(data, productCodeCol))); + // 产品名称(列头未识别或单元格未填时为空串,由服务端按编号自动匹配覆盖) + detail.setProductName(convertToString(getValueSafely(data, productNameCol))); + // 客户编号(新模板新增列,可不填;填写后由服务端校验客户存在性并自动匹配客户名称) + detail.setCustomerCode(convertToString(getValueSafely(data, customerCodeCol))); + // 箱含量/规格 + detail.setBoxCapacity(convertToInteger(getValueSafely(data, specCol))); + // 件数 + detail.setPieceCount(convertToInteger(getValueSafely(data, pieceCountCol))); + // 饮用日期 + detail.setDrinkDate(convertToDate(getValueSafely(data, drinkDateCol))); - LOGGER.debug("第{}行产品明细解析完成,产品编号:{},产品名称:{}", - currentRowNum, detail.getProductCode(), detail.getProductName()); + LOGGER.debug("第{}行产品明细解析完成,产品编号:{},产品名称:{},客户编号:{}", + currentRowNum, detail.getProductCode(), detail.getProductName(), detail.getCustomerCode()); return detail; } @@ -152,9 +278,6 @@ public class StockInExcelListener extends AnalysisEventListener data) { - // 有效条件:产品编号不为空 + 不是合计行 - Object productCodeObj = getValueSafely(data, 0); - // 排除合计行(整行包含"Total"字样) if (containsAnyValue(data, "Total")) { LOGGER.debug("第{}行是合计行,跳过解析", currentRowNum); @@ -162,6 +285,7 @@ public class StockInExcelListener extends AnalysisEventListener data, int columnIndex) { - if (data == null || data.isEmpty()) { + if (columnIndex < 0 || data == null || data.isEmpty()) { return null; } + String strIndex = String.valueOf(columnIndex); + // 1. 直接按key查找(key为String或Integer,跳过非数字key) + for (Object keyObj : data.keySet()) { + if (keyObj != null && strIndex.equals(keyObj.toString())) { + return data.get(keyObj); + } + } + + // 2. 兜底:按顺序位置查找 int index = 0; for (Object value : data.values()) { if (index == columnIndex) { @@ -193,7 +328,7 @@ public class StockInExcelListener extends AnalysisEventListener getProductDetailList() { return productDetailList; } -} \ No newline at end of file +} diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/persistence/ReservationStockInOrderImpl.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/persistence/ReservationStockInOrderImpl.java index 5116ef9b0..2503b839f 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/persistence/ReservationStockInOrderImpl.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/persistence/ReservationStockInOrderImpl.java @@ -172,10 +172,10 @@ public class ReservationStockInOrderImpl extends ServiceImpl> shipperIds = new ArrayList<>(); Set processedShipperIds = new HashSet<>(); + // ====== 物料存在性校验(落库前):查询结果为空说明库中没有该物料,收集全部缺失料号后一次性提示维护 ====== + Map materialCache = new LinkedHashMap<>(); + List missingMaterials = new ArrayList<>(); for (ProductDetail productDetail : productDetailList) { - MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productDetail.getProductCode(),loginUser.getUserPo().getOrganizationId()); - if (materialBaseInfoPO != null && materialBaseInfoPO.getShipperId() != null) { - Long shipperId = materialBaseInfoPO.getShipperId(); - if (processedShipperIds.add(shipperId)) { - Map shipper = new HashMap<>(); - shipper.put("shipperId", shipperId); - shipper.put("shipperName", materialBaseInfoPO.getShipperName()); - shipper.put("shipperCode", materialBaseInfoPO.getShipperCode()); - shipperIds.add(shipper); - } + String productCode = productDetail.getProductCode() == null ? "" : productDetail.getProductCode().trim(); + if (productCode.isEmpty()) { + throw new Exception("出库明细存在未填写產品編號的行,请补全后重新导入"); } + if (materialCache.containsKey(productCode)) { + continue; + } + MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productCode, loginUser.getUserPo().getOrganizationId()); + if (materialBaseInfoPO == null) { + missingMaterials.add(productCode); + } else { + materialCache.put(productCode, materialBaseInfoPO); + } + } + if (!missingMaterials.isEmpty()) { + throw new Exception("物料[" + String.join("、", missingMaterials) + "]不存在,请先维护物料信息后再导入"); + } + for (MaterialBaseInfoPO materialBaseInfoPO : materialCache.values()) { + if (materialBaseInfoPO.getShipperId() != null && processedShipperIds.add(materialBaseInfoPO.getShipperId())) { + Map shipper = new HashMap<>(); + shipper.put("shipperId", materialBaseInfoPO.getShipperId()); + shipper.put("shipperName", materialBaseInfoPO.getShipperName()); + shipper.put("shipperCode", materialBaseInfoPO.getShipperCode()); + shipperIds.add(shipper); + } + } + + // ====== 客户存在性校验(落库前):客户编号在库中查询结果为空说明客户不存在,提示维护后再导入 ====== + Map customerCache = new HashMap<>(); + for (CustomerDeliveryDetail customerDeliveryDetail : customerDeliveryDetailList) { + String customerCode = customerDeliveryDetail.getCustomerCode() == null ? "" : customerDeliveryDetail.getCustomerCode().trim(); + if (customerCode.isEmpty()) { + throw new Exception("客户配送明细存在未填写客户编号的行,请补全后重新导入"); + } + if (customerCache.containsKey(customerCode)) { + continue; + } + UserPo customer = stockOutOrderMapper.queryCustomerByCode(customerCode, loginUser.getUserPo().getOrganizationId()); + if (customer == null || customer.getUserId() == null) { + throw new Exception("客户[" + customerCode + "]不存在,请先维护客户信息后再导入"); + } + customerCache.put(customerCode, customer); } // 货品明细不为空时,校验货主信息 @@ -330,7 +364,8 @@ public class ReservationStockInOrderImpl extends ServiceImpl productDetailList = resultDTO.getProductDetailList(); + if (productDetailList == null || productDetailList.isEmpty()) { + throw new ServiceException("未解析到产品明细数据,请检查模板中是否填写了產品編號"); + } + StockInHeader stockInHeader = resultDTO.getStockInHeader(); + // ====== 物料存在性校验:模板只填物料编号即可,物料名称按编号自动匹配库中数据 ====== + // 查询结果为空说明库中没有该物料,收集全部缺失料号后一次性提示维护 + Map materialCache = new LinkedHashMap<>(); + List missingMaterials = new ArrayList<>(); + for (StockInProductDetail productDetail : productDetailList) { + String productCode = productDetail.getProductCode(); + if (materialCache.containsKey(productCode)) { + continue; + } + MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productCode, loginUser.getUserPo().getOrganizationId()); + if (materialBaseInfoPO == null) { + missingMaterials.add(productCode); + } else { + materialCache.put(productCode, materialBaseInfoPO); + } + } + if (!missingMaterials.isEmpty()) { + throw new ServiceException("物料[" + String.join("、", missingMaterials) + "]不存在,请先维护物料信息后再导入"); + } + // 明细回填库中匹配到的物料名称(自动匹配,模板中的名称列可不填/以库中为准;库中名称为空时保留模板值) + for (StockInProductDetail productDetail : productDetailList) { + String materialName = materialCache.get(productDetail.getProductCode()).getMaterialName(); + if (materialName != null && !materialName.trim().isEmpty()) { + productDetail.setProductName(materialName); + } + } + // ====== 物料归属货主集合(用于客户编号一致性校验,及未填客户编号时的兜底取值) ====== List> shipperIds = new ArrayList<>(); Set processedShipperIds = new HashSet<>(); - for (StockInProductDetail productDetail : productDetailList) { - MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productDetail.getProductCode(),loginUser.getUserPo().getOrganizationId()); - // 物料在当前登录组织下不存在时给出明确提示,避免后续货主取值越界导致500 - if (materialBaseInfoPO == null) { - throw new ServiceException("产品编号[" + productDetail.getProductCode() + "]在当前组织下不存在,请核对产品编号或使用物料所属组织的账号导入"); - } - if (materialBaseInfoPO.getShipperId() != null) { - Long shipperId = materialBaseInfoPO.getShipperId(); - if (processedShipperIds.add(shipperId)) { - Map shipper = new HashMap<>(); - shipper.put("shipperId", shipperId); - shipper.put("shipperName", materialBaseInfoPO.getShipperName()); - shipper.put("shipperCode", materialBaseInfoPO.getShipperCode()); - shipperIds.add(shipper); - } + for (MaterialBaseInfoPO materialBaseInfoPO : materialCache.values()) { + if (materialBaseInfoPO.getShipperId() != null && processedShipperIds.add(materialBaseInfoPO.getShipperId())) { + Map shipper = new HashMap<>(); + shipper.put("shipperId", materialBaseInfoPO.getShipperId()); + shipper.put("shipperName", materialBaseInfoPO.getShipperName()); + shipper.put("shipperCode", materialBaseInfoPO.getShipperCode()); + shipperIds.add(shipper); } } - if (shipperIds.isEmpty()) { - throw new ServiceException("导入的产品均未配置货主,请先在物料档案中维护货主信息"); + if (shipperIds.size() > 1) { + StringBuilder shipperCodeStr = new StringBuilder(); + for (Map shipper : shipperIds) { + if (shipperCodeStr.length() > 0) { + shipperCodeStr.append("、"); + } + shipperCodeStr.append(shipper.get("shipperCode")); + } + throw new ServiceException("导入的物料不属于同一货主(货主编号:" + shipperCodeStr + "),一个入库单只能属于一个货主"); + } + // ====== 客户编号校验与名称自动匹配:模板只填客户编号,客户名称以库中数据为准 ====== + UserPo customer = null; + Set customerCodes = new LinkedHashSet<>(); + for (StockInProductDetail productDetail : productDetailList) { + if (productDetail.getCustomerCode() != null && !productDetail.getCustomerCode().trim().isEmpty()) { + customerCodes.add(productDetail.getCustomerCode().trim()); + } + } + if (customerCodes.size() > 1) { + throw new ServiceException("导入明细中存在多个客户编号(" + String.join("、", customerCodes) + "),一个入库单只能属于一个客户"); + } + if (!customerCodes.isEmpty()) { + String customerCode = customerCodes.iterator().next(); + // 查询结果为空说明库中没有该客户,提示维护后再导入 + customer = stockOutOrderMapper.queryCustomerByCode(customerCode, loginUser.getUserPo().getOrganizationId()); + if (customer == null || customer.getUserId() == null) { + throw new ServiceException("客户[" + customerCode + "]不存在,请先维护客户信息后再导入"); + } + // 客户编号与物料归属货主一致性校验(不一致拦截,提示两者编号;货主编码未维护时提示货主名称) + if (!shipperIds.isEmpty() && !customer.getUserId().equals(shipperIds.get(0).get("shipperId"))) { + Object materialShipperCode = shipperIds.get(0).get("shipperCode"); + String materialShipperDisplay = (materialShipperCode != null && !materialShipperCode.toString().trim().isEmpty()) + ? "货主编号[" + materialShipperCode + "]" : "货主[" + shipperIds.get(0).get("shipperName") + "]"; + throw new ServiceException("客户编号[" + customerCode + "]与物料归属" + materialShipperDisplay + "]不一致,请核对后再导入"); + } + stockInHeader.setCustomerCode(customerCode); + stockInHeader.setCustomerName(customer.getUserName()); } ReservationStockInOrderDO reservationStockInOrderDO = new ReservationStockInOrderDO(); - StockInHeader stockInHeader = resultDTO.getStockInHeader(); // 业务单号:优先使用模板自带OrderNO(不做格式规则校验),与系统已有业务单号重复时提示不允许导入; // 入库业务单号(in_order_number)不使用模板单号,统一由系统原本逻辑生成(见insertDomain); // 模板未填单号时保持系统逻辑:业务单号默认=入库业务单号 @@ -515,9 +612,19 @@ public class ReservationStockInOrderImpl extends ServiceImpl queryOutStockNoticePrintDetail(ReservationStockOutOrderDO stockOutOrderDO); - Long queryShipperId(@Param("userMemberCode") String userMemberCode,@Param("organizationId") Long organizationId); + /** + * 按客户编号+组织查询客户(用户表),返回客户ID/编号/名称 + * 客户编号兼容两种编码:USER.USER_MEMBER_CODE(客户编码)或 USER_SHIPPER.CUSTOMER_NC_CODE(NC客户代码/khdm),任一命中即可 + * 用于出入库导入时校验客户是否存在并自动匹配客户名称;结果为空表示库中无该客户 + */ + UserPo queryCustomerByCode(@Param("userMemberCode") String userMemberCode, @Param("organizationId") Long organizationId); int countByOrderNumberPrefix(@Param("prefix") String prefix); diff --git a/mhd_oms/src/main/resources/mapper/reservationStockOutOrder/ReservationStockOutOrderMapper.xml b/mhd_oms/src/main/resources/mapper/reservationStockOutOrder/ReservationStockOutOrderMapper.xml index 68fd934af..a83d8f6e9 100644 --- a/mhd_oms/src/main/resources/mapper/reservationStockOutOrder/ReservationStockOutOrderMapper.xml +++ b/mhd_oms/src/main/resources/mapper/reservationStockOutOrder/ReservationStockOutOrderMapper.xml @@ -470,8 +470,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + select max(u.user_id) as user_id, max(u.user_name) as user_name, max(u.user_member_code) as user_member_code + from NGWL_TEST_USER."USER" u + left join NGWL_TEST_USER.USER_SHIPPER us + on us.USER_ID = u.USER_ID and us.DEL_FLAG = 1 + where u.ORGANIZATION_ID = #{organizationId} and u.DEL_FLAG = 1 + and (u.USER_MEMBER_CODE = #{userMemberCode} or us.CUSTOMER_NC_CODE = #{userMemberCode})