Commit 57e204bc353b382bbb176d23c85a35ab859d225c

Authored by dadiyang
1 parent 9a41ee23

refactor(common): 基于 Hutool 简化 LocalDateTime 日期解析,判空提前返回,补全边界用例

yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java
1 1 package cn.iocoder.yudao.framework.common.util.json.databind;
2 2  
  3 +import cn.hutool.core.date.DateUtil;
  4 +import cn.hutool.core.date.LocalDateTimeUtil;
  5 +import cn.hutool.core.util.StrUtil;
3 6 import com.fasterxml.jackson.core.JsonParser;
4   -import com.fasterxml.jackson.core.JsonToken;
5 7 import com.fasterxml.jackson.databind.DeserializationContext;
6 8 import com.fasterxml.jackson.databind.JsonDeserializer;
7 9  
8 10 import java.io.IOException;
9 11 import java.time.Instant;
10   -import java.time.LocalDate;
11 12 import java.time.LocalDateTime;
12   -import java.time.OffsetDateTime;
13   -import java.time.ZoneId;
14 13 import java.time.format.DateTimeFormatter;
15   -import java.time.format.DateTimeParseException;
16 14  
17 15 /**
18   - * 基于时间戳的 LocalDateTime 反序列化器
19   - *
20   - * 兼容范围(覆盖前端/接口实际在用的全部格式,避免把"原先能静默入库"的格式变成报错):
21   - * 1. epoch 毫秒:JSON 数字 / 数字字符串(原契约,保持不变)
22   - * 2. 常见日期时间字符串:yyyy-MM-dd HH:mm:ss(含毫秒)
23   - * 3. ISO-8601:yyyy-MM-ddTHH:mm:ss(本地 / 带时区,后者覆盖前端 Date 对象序列化)
24   - * 4. 纯日期:yyyy-MM-dd(当日零点)
25   - * 无法解析时抛清晰错误(含原始值),不再静默落 1970-01-01(消除静默数据损坏)。
  16 + * LocalDateTime 反序列化器,支持毫秒时间戳、常见日期时间字符串和 ISO-8601 日期。
26 17 *
27 18 * @author 老五
28 19 */
... ... @@ -30,51 +21,22 @@ public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDa
30 21  
31 22 public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer();
32 23  
33   - private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
34   - private static final DateTimeFormatter DT_MILLIS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
35   -
36 24 @Override
37 25 public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
38   - // 1. JSON 数字 = epoch 毫秒(原契约,保持)
39   - if (p.currentToken() == JsonToken.VALUE_NUMBER_INT) {
40   - return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getLongValue()), ZoneId.systemDefault());
41   - }
42 26 String text = p.getText();
43   - if (text == null || text.trim().isEmpty()) {
  27 + if (StrUtil.isBlank(text)) {
44 28 return null;
45 29 }
46 30 text = text.trim();
47   - // 1. 数字字符串 = epoch 毫秒
48 31 if (text.matches("-?\\d+")) {
49   - return LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(text)), ZoneId.systemDefault());
50   - }
51   - // 2. yyyy-MM-dd HH:mm:ss / .SSS
52   - try {
53   - return LocalDateTime.parse(text, DT);
54   - } catch (DateTimeParseException ignored) {
55   - }
56   - try {
57   - return LocalDateTime.parse(text, DT_MILLIS);
58   - } catch (DateTimeParseException ignored) {
59   - }
60   - // 3. ISO-8601 本地(2026-09-11T00:00:00 / .SSS)
61   - try {
62   - return LocalDateTime.parse(text);
63   - } catch (DateTimeParseException ignored) {
64   - }
65   - // 3. ISO-8601 带时区(前端 Date 对象序列化:2026-09-11T00:00:00.000Z / +08:00)
66   - try {
67   - return OffsetDateTime.parse(text).atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime();
68   - } catch (DateTimeParseException ignored) {
  32 + return LocalDateTimeUtil.of(Long.parseLong(text));
69 33 }
70   - // 4. 纯日期(2026-09-11)→ 当日零点
71   - try {
72   - return LocalDate.parse(text).atStartOfDay();
73   - } catch (DateTimeParseException ignored) {
  34 + // Hutool 5.x 的自动解析会忽略部分负时区偏移,并截断纳秒。
  35 + if (text.contains("T")) {
  36 + return LocalDateTimeUtil.of(DateTimeFormatter.ISO_DATE_TIME.parseBest(
  37 + text, Instant::from, LocalDateTime::from));
74 38 }
75   - // 无法解析:抛清晰错误(不再静默落 1970)
76   - throw new IllegalArgumentException("无法解析时间值:" + text
77   - + "(支持 epoch 毫秒 / yyyy-MM-dd HH:mm:ss / ISO-8601 / yyyy-MM-dd)");
  39 + return LocalDateTimeUtil.of(DateUtil.parse(text).toInstant());
78 40 }
79 41  
80 42 }
... ...
yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializerTest.java
... ... @@ -63,6 +63,12 @@ public class TimestampLocalDateTimeDeserializerTest {
63 63 }
64 64  
65 65 @Test
  66 + public void testIsoLocalWithNanos() throws Exception {
  67 + assertEquals(LocalDateTime.of(2026, 9, 11, 8, 0, 0, 123456789),
  68 + parse("\"2026-09-11T08:00:00.123456789\""));
  69 + }
  70 +
  71 + @Test
66 72 public void testIsoWithOffset() throws Exception {
67 73 // 前端 Date 对象序列化:带 Z(UTC)。2026-09-11T00:00:00Z 换算到系统时区
68 74 LocalDateTime expected = java.time.OffsetDateTime.parse("2026-09-11T00:00:00Z")
... ... @@ -92,16 +98,45 @@ public class TimestampLocalDateTimeDeserializerTest {
92 98 }
93 99  
94 100 @Test
  101 + public void testIsoWithNegativeOffsetAndMillis() throws Exception {
  102 + LocalDateTime expected = java.time.OffsetDateTime.parse("2026-09-11T23:30:00.123-05:30")
  103 + .atZoneSameInstant(java.time.ZoneId.systemDefault()).toLocalDateTime();
  104 + assertEquals(expected, parse("\"2026-09-11T23:30:00.123-05:30\""));
  105 + }
  106 +
  107 + @Test
  108 + public void testZeroAndNegativeEpochMilli() throws Exception {
  109 + for (long millis : new long[]{0L, -1L}) {
  110 + LocalDateTime expected = LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(millis),
  111 + java.time.ZoneId.systemDefault());
  112 + assertEquals(expected, parse(Long.toString(millis)));
  113 + assertEquals(expected, parse("\"" + millis + "\""));
  114 + }
  115 + }
  116 +
  117 + @Test
  118 + public void testSurroundingWhitespace() throws Exception {
  119 + assertEquals(LocalDateTime.of(2026, 9, 11, 8, 0, 0), parse("\" 2026-09-11 08:00:00 \""));
  120 + }
  121 +
  122 + @Test
95 123 public void testNullAndEmpty() throws Exception {
96 124 assertNull(parse("null"));
97 125 assertNull(parse("\"\""));
  126 + assertNull(parse("\" \""));
  127 + }
  128 +
  129 + @Test
  130 + public void testHutoolDateFormats() throws Exception {
  131 + assertEquals(LocalDateTime.of(2026, 9, 11, 0, 0), parse("\"2026/09/11\""));
  132 + assertEquals(LocalDateTime.of(2026, 9, 11, 8, 0), parse("\"2026/09/11 08:00:00\""));
98 133 }
99 134  
100 135 @Test
101 136 public void testInvalidThrows() {
102 137 // 非法输入抛清晰错误,不再静默落 1970
103 138 assertThrows(Exception.class, () -> parse("\"not-a-date\""));
104   - assertThrows(Exception.class, () -> parse("\"2026/09/11\""));
  139 +
105 140 }
106 141  
107 142 }
... ...