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)
|
// 支持的日期格式(用于String转Date)
|
||||||
private static final String[] SUPPORTED_DATE_FORMATS = {
|
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("第{}行数据转换异常,列号:{},列名:{}",
|
LOGGER.error("第{}行数据转换异常,列号:{},列名:{}",
|
||||||
currentRowNum, e.getColumnIndex() + 1, getColumnNameByIndex(data, e.getColumnIndex()), e);
|
currentRowNum, e.getColumnIndex() + 1, getColumnNameByIndex(data, e.getColumnIndex()), e);
|
||||||
throw new RuntimeException("第" + currentRowNum + "行列" + (e.getColumnIndex() + 1) + "数据格式错误", e);
|
throw new RuntimeException("第" + currentRowNum + "行列" + (e.getColumnIndex() + 1) + "数据格式错误", e);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
// convertToDate等抛出的带明确提示的异常直接透传,避免被通用信息掩盖
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 其他解析异常
|
// 其他解析异常
|
||||||
LOGGER.error("第{}行解析异常,数据:{}", currentRowNum, data, 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) {
|
private Date convertToDate(Object value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
@@ -234,20 +242,45 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
|||||||
return (Date) value;
|
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) {
|
if (value instanceof String) {
|
||||||
String strValue = ((String) value).trim();
|
String strValue = ((String) value).trim();
|
||||||
|
if (strValue.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return DateUtils.parseDate(strValue, SUPPORTED_DATE_FORMATS);
|
Date parsed = DateUtils.parseDateStrictly(strValue, SUPPORTED_DATE_FORMATS);
|
||||||
|
validateYearRange(parsed, strValue);
|
||||||
|
return parsed;
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
LOGGER.warn("值转换为Date失败,值:{},支持格式:{}",
|
LOGGER.error("值转换为Date失败,值:{},支持格式:{}",
|
||||||
strValue, String.join(",", SUPPORTED_DATE_FORMATS), e);
|
strValue, String.join(",", SUPPORTED_DATE_FORMATS), e);
|
||||||
|
throw new RuntimeException("第" + currentRowNum + "行日期格式无法识别:'" + strValue
|
||||||
|
+ "',请使用如 2026/08/25、2026-08-25、2026年8月25日 等格式", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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层获取解析结果) ----------------------
|
// ---------------------- Getter方法(供Service层获取解析结果) ----------------------
|
||||||
public StockInHeader getStockInHeader() {
|
public StockInHeader getStockInHeader() {
|
||||||
return stockInHeader;
|
return stockInHeader;
|
||||||
|
|||||||
+39
-6
@@ -26,9 +26,13 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
|||||||
private final StockInHeader stockInHeader = new StockInHeader();
|
private final StockInHeader stockInHeader = new StockInHeader();
|
||||||
private final List<StockInProductDetail> productDetailList = new ArrayList<>();
|
private final List<StockInProductDetail> productDetailList = new ArrayList<>();
|
||||||
|
|
||||||
// 支持的日期格式(用于String转Date)
|
// 支持的日期格式(用于String转Date),覆盖中文/港式(日/月/年)/美式(月/日/年)/欧式等常见写法
|
||||||
private static final String[] SUPPORTED_DATE_FORMATS = {
|
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("第{}行数据转换异常,列号:{},列名:{}",
|
LOGGER.error("第{}行数据转换异常,列号:{},列名:{}",
|
||||||
currentRowNum, e.getColumnIndex() + 1, getColumnNameByIndex(data, e.getColumnIndex()), e);
|
currentRowNum, e.getColumnIndex() + 1, getColumnNameByIndex(data, e.getColumnIndex()), e);
|
||||||
throw new RuntimeException("第" + currentRowNum + "行列" + (e.getColumnIndex() + 1) + "数据格式错误", e);
|
throw new RuntimeException("第" + currentRowNum + "行列" + (e.getColumnIndex() + 1) + "数据格式错误", e);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
// convertToDate等抛出的带明确提示的异常直接透传,避免被通用信息掩盖
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 其他解析异常
|
// 其他解析异常
|
||||||
LOGGER.error("第{}行解析异常,数据:{}", currentRowNum, data, 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) {
|
private Date convertToDate(Object value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
@@ -274,20 +282,45 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
|||||||
return (Date) value;
|
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) {
|
if (value instanceof String) {
|
||||||
String strValue = ((String) value).trim();
|
String strValue = ((String) value).trim();
|
||||||
|
if (strValue.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return DateUtils.parseDate(strValue, SUPPORTED_DATE_FORMATS);
|
Date parsed = DateUtils.parseDateStrictly(strValue, SUPPORTED_DATE_FORMATS);
|
||||||
|
validateYearRange(parsed, strValue);
|
||||||
|
return parsed;
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
LOGGER.warn("值转换为Date失败,值:{},支持格式:{}",
|
LOGGER.error("值转换为Date失败,值:{},支持格式:{}",
|
||||||
strValue, String.join(",", SUPPORTED_DATE_FORMATS), e);
|
strValue, String.join(",", SUPPORTED_DATE_FORMATS), e);
|
||||||
|
throw new RuntimeException("第" + currentRowNum + "行日期格式无法识别:'" + strValue
|
||||||
|
+ "',请使用如 2026/08/25、2026-08-25、2026年8月25日 等格式", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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层获取解析结果) ----------------------
|
// ---------------------- Getter方法(供Service层获取解析结果) ----------------------
|
||||||
public StockInHeader getStockInHeader() {
|
public StockInHeader getStockInHeader() {
|
||||||
return stockInHeader;
|
return stockInHeader;
|
||||||
|
|||||||
+10
@@ -367,6 +367,16 @@ public class ReservationStockInOrderPO extends BaseVOEntity {
|
|||||||
@Excel(name = "下单日期", width = 30, dateFormat = "yyyy-MM-dd")
|
@Excel(name = "下单日期", width = 30, dateFormat = "yyyy-MM-dd")
|
||||||
private Date orderDate;
|
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("业务单号")
|
@ApiModelProperty("业务单号")
|
||||||
@Excel(name = "业务单号")
|
@Excel(name = "业务单号")
|
||||||
|
|||||||
+9
-3
@@ -251,11 +251,17 @@ public class ReservationStockInOrderApi extends BaseController {
|
|||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
// 参数错误(如文件为空)
|
// 参数错误(如文件为空)
|
||||||
// LOGGER.error("Excel解析参数错误:{}", e.getMessage());
|
// 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) {
|
} catch (Exception e) {
|
||||||
// 其他解析异常
|
// 其他解析异常(如日期格式无法识别),返回具体原因便于用户修正文件
|
||||||
// LOGGER.error("Excel解析失败:{}", e.getMessage(), 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="discount" column="DISCOUNT" />
|
||||||
<result property="isFreeManpowerFee" column="IS_FREE_MANPOWER_FEE" />
|
<result property="isFreeManpowerFee" column="IS_FREE_MANPOWER_FEE" />
|
||||||
<result property="executionInOrderNumber" column="EXECUTION_IN_ORDER_NUMBER" />
|
<result property="executionInOrderNumber" column="EXECUTION_IN_ORDER_NUMBER" />
|
||||||
|
<!-- 饮用有效日期:queryList联查明细表聚合带出,其他查询无此列时不填充 -->
|
||||||
|
<result property="drinkDate" column="drink_date" />
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
|
|
||||||
@@ -513,8 +515,14 @@
|
|||||||
|
|
||||||
<select id="queryList" parameterType="com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO" resultMap="StockInOrderResult">
|
<select id="queryList" parameterType="com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO" resultMap="StockInOrderResult">
|
||||||
select
|
select
|
||||||
<include refid="selectStockInOrderPo"/>
|
<include refid="selectStockInOrderPo"/>, t.drink_date
|
||||||
from reservation_stock_in_order a
|
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>
|
<where>
|
||||||
<include refid="selectStockInOrderPo1"/>
|
<include refid="selectStockInOrderPo1"/>
|
||||||
</where>
|
</where>
|
||||||
|
|||||||
Reference in New Issue
Block a user