diff --git a/src/main/java/com/xujie/modules/order/support/PoImportRowMapper.java b/src/main/java/com/xujie/modules/order/support/PoImportRowMapper.java index 5fa9da5..f3fec85 100644 --- a/src/main/java/com/xujie/modules/order/support/PoImportRowMapper.java +++ b/src/main/java/com/xujie/modules/order/support/PoImportRowMapper.java @@ -1,23 +1,118 @@ package com.xujie.modules.order.support; import com.xujie.modules.order.vo.PODetailVo; +import org.apache.poi.ss.usermodel.DateUtil; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; import org.springframework.util.StringUtils; import java.math.BigDecimal; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.util.Arrays; import java.util.Date; +import java.util.List; +import java.util.Locale; import java.util.Map; +/** + * 将 PO 导入预览行转换成 PODetailVo。 + * 日期字段兼容 Excel 常见文本,不再只接受 yyyy-MM-dd。 + */ public final class PoImportRowMapper { - private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("yyyy-MM-dd"); + /** + * 两位年份窗口起点:以当前年往前 80 年。 + * 例如 2026 年导入时,22 解释为 2022,98 解释为 1998。 + */ + private static final int TWO_DIGIT_YEAR_BASE = LocalDate.now().getYear() - 80; + + private static final List DATE_FORMATTERS = Arrays.asList( + DateTimeFormatter.ofPattern("yyyy-M-d", Locale.ENGLISH), + DateTimeFormatter.ofPattern("yyyy/M/d", Locale.ENGLISH), + DateTimeFormatter.ofPattern("yyyy.M.d", Locale.ENGLISH), + DateTimeFormatter.ofPattern("yyyyMMdd", Locale.ENGLISH), + DateTimeFormatter.ofPattern("M/d/yyyy", Locale.ENGLISH), + DateTimeFormatter.ofPattern("M-d-yyyy", Locale.ENGLISH), + DateTimeFormatter.ofPattern("M.d.yyyy", Locale.ENGLISH), + reducedYearFormatter("M/d/"), + reducedYearFormatter("M-d-"), + reducedYearFormatter("M.d."), + DateTimeFormatter.ofPattern("d/M/yyyy", Locale.ENGLISH), + DateTimeFormatter.ofPattern("d-M-yyyy", Locale.ENGLISH), + DateTimeFormatter.ofPattern("d.M.yyyy", Locale.ENGLISH), + DateTimeFormatter.ofPattern("d-MMM-yyyy", Locale.ENGLISH), + reducedMonthNameFormatter("d-MMM-"), + DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.ENGLISH), + DateTimeFormatter.ofPattern("yyyy年M月d日", Locale.CHINA), + reducedChineseMonthFormatter() + ); + + private static final List DATE_TIME_FORMATTERS = Arrays.asList( + DateTimeFormatter.ofPattern("yyyy-M-d H:m:s", Locale.ENGLISH), + DateTimeFormatter.ofPattern("yyyy/M/d H:m:s", Locale.ENGLISH), + DateTimeFormatter.ofPattern("yyyy-M-d H:m", Locale.ENGLISH), + DateTimeFormatter.ofPattern("yyyy/M/d H:m", Locale.ENGLISH), + DateTimeFormatter.ofPattern("M/d/yyyy H:m:s", Locale.ENGLISH), + DateTimeFormatter.ofPattern("M/d/yyyy H:m", Locale.ENGLISH), + reducedDateTimeFormatter("M/d/", " H:m:s"), + reducedDateTimeFormatter("M/d/", " H:m"), + DateTimeFormatter.ofPattern("M/d/yyyy h:m:s a", Locale.ENGLISH), + DateTimeFormatter.ofPattern("yyyy-M-d'T'H:m:s", Locale.ENGLISH) + ); private PoImportRowMapper() { } + /** + * 构造带两位年的日期格式。 + * 入参 prefix 为“月日分隔”前缀,例如 {@code M/d/},后面再接 22 这种两位年。 + */ + private static DateTimeFormatter reducedYearFormatter(String prefix) { + return new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendPattern(prefix) + .appendValueReduced(ChronoField.YEAR, 2, 2, TWO_DIGIT_YEAR_BASE) + .toFormatter(Locale.ENGLISH); + } + + /** + * 构造 {@code 13-Oct-22} 这类英文月份 + 两位年格式。 + */ + private static DateTimeFormatter reducedMonthNameFormatter(String prefix) { + return new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendPattern(prefix) + .appendValueReduced(ChronoField.YEAR, 2, 2, TWO_DIGIT_YEAR_BASE) + .toFormatter(Locale.ENGLISH); + } + + /** + * 构造 {@code 13-10月-22} 这类中文月份 + 两位年格式。 + */ + private static DateTimeFormatter reducedChineseMonthFormatter() { + return new DateTimeFormatterBuilder() + .appendPattern("d-M月-") + .appendValueReduced(ChronoField.YEAR, 2, 2, TWO_DIGIT_YEAR_BASE) + .toFormatter(Locale.CHINA); + } + + /** + * 构造带时分秒的两位年日期时间格式,兼容 Excel 读出的 {@code 10/13/22 0:00:00}。 + */ + private static DateTimeFormatter reducedDateTimeFormatter(String datePrefix, String timePattern) { + return new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendPattern(datePrefix) + .appendValueReduced(ChronoField.YEAR, 2, 2, TWO_DIGIT_YEAR_BASE) + .appendPattern(timePattern) + .toFormatter(Locale.ENGLISH); + } + public static PODetailVo toDetailVo(Map row, String site, String customer) { PODetailVo vo = new PODetailVo(); vo.setSite(site); @@ -70,21 +165,32 @@ public final class PoImportRowMapper { return null; } if (fieldCode.startsWith("ext_dt") || Date.class.equals(type)) { - try { - return DATE_FMT.parse(text.length() > 10 ? text.substring(0, 10) : text); - } catch (ParseException ex) { - throw new IllegalArgumentException("日期格式错误(" + fieldCode + "): " + text); - } + return parseImportDate(fieldCode, text); } if (Number.class.isAssignableFrom(type) || type == double.class || type == Double.class || type == int.class || type == Integer.class) { - if (BigDecimal.class.equals(type)) { - return new BigDecimal(text); + String numberText = normalizeImportNumberText(text); + if (!StringUtils.hasText(numberText)) { + return null; } - if (type == Integer.class || type == int.class) { - return Integer.valueOf(text.contains(".") ? text.substring(0, text.indexOf('.')) : text); + try { + if (BigDecimal.class.equals(type)) { + return new BigDecimal(numberText); + } + if (type == Integer.class || type == int.class) { + String integerText = numberText.contains(".") + ? numberText.substring(0, numberText.indexOf('.')) + : numberText; + return Integer.valueOf(integerText); + } + return Double.valueOf(numberText); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("数字格式错误(" + fieldCode + "): " + text); } - return Double.valueOf(text); + } + if ("value".equals(fieldCode)) { + // P_Value 是字符串落库,但仍按数字处理:去掉 10,000.00 中的千分位逗号。 + return normalizeImportNumberText(text); } if ("part_no".equals(fieldCode) && !StringUtils.hasText(text)) { return null; @@ -101,4 +207,134 @@ public final class PoImportRowMapper { } return text; } + + /** + * 去掉导入数字里的千分位逗号(含中文逗号、不间断空格)。 + * 入参:Excel 读出的原始数字文本,例如 10,000.00。 + * 出参:可供 BigDecimal/Double/Integer 解析的文本,例如 10000.00。 + */ + private static String normalizeImportNumberText(String text) { + if (text == null) { + return ""; + } + return text.replace('\u00A0', ' ') + .replace(",", "") + .replace(",", "") + .trim(); + } + + /** + * 解析 PO 导入日期。 + * 入参:fieldCode 用于错误提示;rawText 为 Excel DataFormatter 读出的单元格文本。 + * 出参:仅保留日期部分的 java.sql.Date。 + * 兼容:yyyy-MM-dd、yyyy/M/d、M/d/yyyy、10/13/22、13-Oct-22、2022年10月13日,以及带时分秒的同类文本。 + * 斜杠/短日期优先按美式月/日解释,因此 10/13/22 为 2022-10-13,而不是日/月。 + * 失败时抛 IllegalArgumentException,预览行黄标,保存接口同样拦截。 + */ + private static Date parseImportDate(String fieldCode, String rawText) { + String text = normalizeDateText(rawText); + if (!StringUtils.hasText(text)) { + return null; + } + LocalDate localDate = tryParseLocalDate(text); + if (localDate == null && (text.contains(" ") || text.contains("T"))) { + String datePart = text.split("[ T]")[0]; + localDate = tryParseLocalDate(datePart); + } + if (localDate != null) { + return java.sql.Date.valueOf(localDate); + } + Date serialDate = tryParseExcelSerial(text); + if (serialDate != null) { + return serialDate; + } + throw new IllegalArgumentException("日期格式错误(" + fieldCode + "): " + rawText); + } + + /** + * 去掉不间断空格、首尾空白,便于匹配多种日期文本。 + */ + private static String normalizeDateText(String rawText) { + if (rawText == null) { + return ""; + } + return rawText.replace('\u00A0', ' ').trim(); + } + + /** + * 依次尝试日期、日期时间格式。任意一种成功即返回 LocalDate,全部失败返回 null。 + * 两位年若被解析成公元 22 年,会按 80 年窗口折算到 1922-2022 这一段业务年份。 + */ + private static LocalDate tryParseLocalDate(String text) { + for (DateTimeFormatter formatter : DATE_FORMATTERS) { + LocalDate parsed = acceptBusinessDate(tryParseAsLocalDate(text, formatter)); + if (parsed != null) { + return parsed; + } + } + for (DateTimeFormatter formatter : DATE_TIME_FORMATTERS) { + LocalDate parsed = acceptBusinessDate(tryParseAsLocalDateTime(text, formatter)); + if (parsed != null) { + return parsed; + } + } + return null; + } + + private static LocalDate tryParseAsLocalDate(String text, DateTimeFormatter formatter) { + try { + return LocalDate.parse(text, formatter); + } catch (DateTimeParseException ignored) { + return null; + } + } + + private static LocalDate tryParseAsLocalDateTime(String text, DateTimeFormatter formatter) { + try { + return LocalDateTime.parse(text, formatter).toLocalDate(); + } catch (DateTimeParseException ignored) { + return null; + } + } + + /** + * 只接受业务上合理的年份。 + * 入参可能是完整年(2022)或被当成公元年的两位年(22)。 + * 出参:1900-2100 之间的日期;无法折算则返回 null,让调用方继续试下一种格式。 + */ + private static LocalDate acceptBusinessDate(LocalDate date) { + if (date == null) { + return null; + } + int year = date.getYear(); + if (year >= 0 && year <= 99) { + int base = TWO_DIGIT_YEAR_BASE; + int fullYear = base + Math.floorMod(year - Math.floorMod(base, 100), 100); + date = date.withYear(fullYear); + year = date.getYear(); + } + if (year < 1900 || year > 2100) { + return null; + } + return date; + } + + /** + * 兼容 Excel 把日期单元格格式化成序列号的情况,例如 44847。 + * 仅接受合理业务区间,避免把普通数字误当成日期。 + */ + private static Date tryParseExcelSerial(String text) { + if (!text.matches("\\d{5}(?:\\.\\d+)?")) { + return null; + } + try { + double serial = Double.parseDouble(text); + if (serial < 20000d || serial > 80000d) { + return null; + } + return DateUtil.getJavaDate(serial); + } catch (RuntimeException ignored) { + return null; + } + } }