|
|
|
@ -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<DateTimeFormatter> 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<DateTimeFormatter> 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<String, Object> 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; |
|
|
|
} |
|
|
|
} |
|
|
|
} |