feat(oms):入库导入模板饮用日期解析修复,入库单列表联查带出饮用有效日期

1. 入库/执行入库Excel解析监听器:扩充日期格式支持(yyyy年M月d日、d/M/yyyy、M/d/yyyy、dd.MM.yyyy等16种),
   改为严格解析防止宽松解析产生公元1~99年脏数据,新增Excel日期序列号兜底及年份合理性校验,
   解析失败抛出带行号的明确提示而非静默置空
2. parse-excel接口异常时返回具体错误信息(原为空body 500)
3. 入库单列表queryList联查明细表取最早饮用日期(MIN drink_date),PO及resultMap补drinkDate映射
This commit is contained in:
rcx
2026-08-25 15:07:04 +08:00
parent 83f4cce451
commit 735cf9af9f
5 changed files with 105 additions and 15 deletions
@@ -28,7 +28,11 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
// 支持的日期格式(用于String转Date)
private static final String[] SUPPORTED_DATE_FORMATS = {
"yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "yyyy-MM-dd HH:mm:ss"
"yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "yyyy-MM-dd HH:mm:ss",
"yyyy/M/d", "yyyy.M.d", "yyyy-M-d",
"yyyy年M月d日", "yyyy年MM月dd日",
"d/M/yyyy", "d/M/yy", "M/d/yyyy", "M/d/yy",
"dd.MM.yyyy", "d-MMM-yyyy", "yyyyMMddHHmmss"
};
// 行号标记(用于区分第一行和表格行)
@@ -57,6 +61,9 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
LOGGER.error("第{}行数据转换异常,列号:{},列名:{}",
currentRowNum, e.getColumnIndex() + 1, getColumnNameByIndex(data, e.getColumnIndex()), e);
throw new RuntimeException("" + currentRowNum + "行列" + (e.getColumnIndex() + 1) + "数据格式错误", e);
} catch (RuntimeException e) {
// convertToDate等抛出的带明确提示的异常直接透传,避免被通用信息掩盖
throw e;
} catch (Exception e) {
// 其他解析异常
LOGGER.error("第{}行解析异常,数据:{}", currentRowNum, data, e);
@@ -222,7 +229,8 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
}
/**
* 通用转换:Object转Date(支持Date类型、String类型)
* 通用转换:Object转Date(支持Date类型、Number类型(Excel日期序列号)、String类型)
* 单元格有值但所有格式均无法识别时抛出异常,避免饮用日期被静默丢弃
*/
private Date convertToDate(Object value) {
if (value == null) {
@@ -234,20 +242,45 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
return (Date) value;
}
// String类型转Date(支持多种格式)
// Number类型:Excel日期本质是序列号数字,直接按序列号转换
if (value instanceof Number) {
return org.apache.poi.ss.usermodel.DateUtil.getJavaDate(((Number) value).doubleValue());
}
// String类型转Date(支持多种格式,严格解析防止宽松解析产生公元1~99年的脏数据)
if (value instanceof String) {
String strValue = ((String) value).trim();
if (strValue.isEmpty()) {
return null;
}
try {
return DateUtils.parseDate(strValue, SUPPORTED_DATE_FORMATS);
Date parsed = DateUtils.parseDateStrictly(strValue, SUPPORTED_DATE_FORMATS);
validateYearRange(parsed, strValue);
return parsed;
} catch (ParseException e) {
LOGGER.warn("值转换为Date失败,值:{},支持格式:{}",
LOGGER.error("值转换为Date失败,值:{},支持格式:{}",
strValue, String.join(",", SUPPORTED_DATE_FORMATS), e);
throw new RuntimeException("" + currentRowNum + "行日期格式无法识别:'" + strValue
+ "',请使用如 2026/08/25、2026-08-25、2026年8月25日 等格式", e);
}
}
return null;
}
/**
* 年份合理性校验:两位年份(如26/12/26)会被解析成0026年,此类结果视为无效并提示用户
*/
private void validateYearRange(Date date, String rawValue) {
java.util.Calendar cal = java.util.Calendar.getInstance();
cal.setTime(date);
int year = cal.get(java.util.Calendar.YEAR);
if (year < 1900 || year > 2100) {
throw new RuntimeException("" + currentRowNum + "行日期格式无法识别:'" + rawValue
+ "',请使用4位年份,如 2026/08/25、2026年8月25日");
}
}
// ---------------------- Getter方法(供Service层获取解析结果) ----------------------
public StockInHeader getStockInHeader() {
return stockInHeader;
@@ -26,9 +26,13 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
private final StockInHeader stockInHeader = new StockInHeader();
private final List<StockInProductDetail> productDetailList = new ArrayList<>();
// 支持的日期格式(用于String转Date)
// 支持的日期格式(用于String转Date),覆盖中文/港式(日/月/年)/美式(月/日/年)/欧式等常见写法
private static final String[] SUPPORTED_DATE_FORMATS = {
"yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "yyyy-MM-dd HH:mm:ss"
"yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "yyyy-MM-dd HH:mm:ss",
"yyyy/M/d", "yyyy.M.d", "yyyy-M-d",
"yyyy年M月d日", "yyyy年MM月dd日",
"d/M/yyyy", "d/M/yy", "M/d/yyyy", "M/d/yy",
"dd.MM.yyyy", "d-MMM-yyyy", "yyyyMMddHHmmss"
};
// 行号标记(用于区分第一行和表格行)
@@ -65,6 +69,9 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
LOGGER.error("第{}行数据转换异常,列号:{},列名:{}",
currentRowNum, e.getColumnIndex() + 1, getColumnNameByIndex(data, e.getColumnIndex()), e);
throw new RuntimeException("" + currentRowNum + "行列" + (e.getColumnIndex() + 1) + "数据格式错误", e);
} catch (RuntimeException e) {
// convertToDate等抛出的带明确提示的异常直接透传,避免被通用信息掩盖
throw e;
} catch (Exception e) {
// 其他解析异常
LOGGER.error("第{}行解析异常,数据:{}", currentRowNum, data, e);
@@ -262,7 +269,8 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
}
/**
* 通用转换:Object转Date(支持Date类型、String类型)
* 通用转换:Object转Date(支持Date类型、Number类型(Excel日期序列号)、String类型)
* 单元格有值但所有格式均无法识别时抛出异常,避免饮用日期被静默丢弃
*/
private Date convertToDate(Object value) {
if (value == null) {
@@ -274,20 +282,45 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
return (Date) value;
}
// String类型转Date(支持多种格式)
// Number类型:Excel日期本质是序列号数字,直接按序列号转换
if (value instanceof Number) {
return org.apache.poi.ss.usermodel.DateUtil.getJavaDate(((Number) value).doubleValue());
}
// String类型转Date(支持多种格式,严格解析防止宽松解析产生公元1~99年的脏数据)
if (value instanceof String) {
String strValue = ((String) value).trim();
if (strValue.isEmpty()) {
return null;
}
try {
return DateUtils.parseDate(strValue, SUPPORTED_DATE_FORMATS);
Date parsed = DateUtils.parseDateStrictly(strValue, SUPPORTED_DATE_FORMATS);
validateYearRange(parsed, strValue);
return parsed;
} catch (ParseException e) {
LOGGER.warn("值转换为Date失败,值:{},支持格式:{}",
LOGGER.error("值转换为Date失败,值:{},支持格式:{}",
strValue, String.join(",", SUPPORTED_DATE_FORMATS), e);
throw new RuntimeException("" + currentRowNum + "行日期格式无法识别:'" + strValue
+ "',请使用如 2026/08/25、2026-08-25、2026年8月25日 等格式", e);
}
}
return null;
}
/**
* 年份合理性校验:两位年份(如26/12/26)会被解析成0026年,此类结果视为无效并提示用户
*/
private void validateYearRange(Date date, String rawValue) {
java.util.Calendar cal = java.util.Calendar.getInstance();
cal.setTime(date);
int year = cal.get(java.util.Calendar.YEAR);
if (year < 1900 || year > 2100) {
throw new RuntimeException("" + currentRowNum + "行日期格式无法识别:'" + rawValue
+ "',请使用4位年份,如 2026/08/25、2026年8月25日");
}
}
// ---------------------- Getter方法(供Service层获取解析结果) ----------------------
public StockInHeader getStockInHeader() {
return stockInHeader;
@@ -367,6 +367,16 @@ public class ReservationStockInOrderPO extends BaseVOEntity {
@Excel(name = "下单日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date orderDate;
/**
* 饮用有效日期:来自明细(reservation_in_material_detail.drink_date)的聚合值(最早)
* 仅列表查询(queryList)联查带出,主表无此列
*/
@ApiModelProperty("饮用有效日期(明细最早饮用日期)")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@Excel(name = "饮用有效日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date drinkDate;
@ApiModelProperty("业务单号")
@Excel(name = "业务单号")
@@ -251,11 +251,17 @@ public class ReservationStockInOrderApi extends BaseController {
} catch (IllegalArgumentException e) {
// 参数错误(如文件为空)
// LOGGER.error("Excel解析参数错误:{}", e.getMessage());
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
Map<String, Object> result = new HashMap<>();
result.put("code", 400);
result.put("msg", e.getMessage());
return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
} catch (Exception e) {
// 其他解析异常
// 其他解析异常(如日期格式无法识别),返回具体原因便于用户修正文件
// LOGGER.error("Excel解析失败:{}", e.getMessage(), e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
Map<String, Object> result = new HashMap<>();
result.put("code", 500);
result.put("msg", e.getMessage());
return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@@ -109,6 +109,8 @@
<result property="discount" column="DISCOUNT" />
<result property="isFreeManpowerFee" column="IS_FREE_MANPOWER_FEE" />
<result property="executionInOrderNumber" column="EXECUTION_IN_ORDER_NUMBER" />
<!-- 饮用有效日期:queryList联查明细表聚合带出,其他查询无此列时不填充 -->
<result property="drinkDate" column="drink_date" />
</resultMap>
@@ -513,8 +515,14 @@
<select id="queryList" parameterType="com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO" resultMap="StockInOrderResult">
select
<include refid="selectStockInOrderPo"/>
<include refid="selectStockInOrderPo"/>, t.drink_date
from reservation_stock_in_order a
<!-- 饮用有效日期:取明细表最早饮用日期(一张单多行明细日期不同时按最早到期口径展示) -->
left join (select in_order_number, min(drink_date) as drink_date
from reservation_in_material_detail
where del_flag = 1 and drink_date is not null
group by in_order_number) t
on a.in_order_number = t.in_order_number
<where>
<include refid="selectStockInOrderPo1"/>
</where>