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:
+38
-5
@@ -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;
|
||||
|
||||
+39
-6
@@ -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;
|
||||
|
||||
+10
@@ -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 = "业务单号")
|
||||
|
||||
+9
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user