Commit d68047bb6481c96a18f5d9d71c5e5c61be6ef7f4

Authored by 芋道源码
Committed by GitHub
2 parents c3b5478a 57e204bc

Merge pull request #1251 from dadiyang/fix/a57-localdatetime-robust

[BUG] MES 库存调拨:新建调拨单选了转移日期、保存提示成功,但日期存成 1970-01-01、列表显示空白
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java
1 package cn.iocoder.yudao.framework.common.util.json.databind; 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 import com.fasterxml.jackson.core.JsonParser; 6 import com.fasterxml.jackson.core.JsonParser;
4 import com.fasterxml.jackson.databind.DeserializationContext; 7 import com.fasterxml.jackson.databind.DeserializationContext;
5 import com.fasterxml.jackson.databind.JsonDeserializer; 8 import com.fasterxml.jackson.databind.JsonDeserializer;
@@ -7,10 +10,10 @@ import com.fasterxml.jackson.databind.JsonDeserializer; @@ -7,10 +10,10 @@ import com.fasterxml.jackson.databind.JsonDeserializer;
7 import java.io.IOException; 10 import java.io.IOException;
8 import java.time.Instant; 11 import java.time.Instant;
9 import java.time.LocalDateTime; 12 import java.time.LocalDateTime;
10 -import java.time.ZoneId; 13 +import java.time.format.DateTimeFormatter;
11 14
12 /** 15 /**
13 - * 基于时间戳的 LocalDateTime 反序列化器 16 + * LocalDateTime 反序列化器,支持毫秒时间戳、常见日期时间字符串和 ISO-8601 日期。
14 * 17 *
15 * @author 老五 18 * @author 老五
16 */ 19 */
@@ -20,8 +23,20 @@ public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDa @@ -20,8 +23,20 @@ public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDa
20 23
21 @Override 24 @Override
22 public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { 25 public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
23 - // 将 Long 时间戳,转换为 LocalDateTime 对象  
24 - return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); 26 + String text = p.getText();
  27 + if (StrUtil.isBlank(text)) {
  28 + return null;
  29 + }
  30 + text = text.trim();
  31 + if (text.matches("-?\\d+")) {
  32 + return LocalDateTimeUtil.of(Long.parseLong(text));
  33 + }
  34 + // Hutool 5.x 的自动解析会忽略部分负时区偏移,并截断纳秒。
  35 + if (text.contains("T")) {
  36 + return LocalDateTimeUtil.of(DateTimeFormatter.ISO_DATE_TIME.parseBest(
  37 + text, Instant::from, LocalDateTime::from));
  38 + }
  39 + return LocalDateTimeUtil.of(DateUtil.parse(text).toInstant());
25 } 40 }
26 41
27 } 42 }
yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializerTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.common.util.json.databind;
  2 +
  3 +import com.fasterxml.jackson.databind.ObjectMapper;
  4 +import com.fasterxml.jackson.databind.module.SimpleModule;
  5 +import org.junit.jupiter.api.Test;
  6 +
  7 +import java.time.LocalDateTime;
  8 +
  9 +import static org.junit.jupiter.api.Assertions.assertEquals;
  10 +import static org.junit.jupiter.api.Assertions.assertThrows;
  11 +import static org.junit.jupiter.api.Assertions.assertNull;
  12 +
  13 +/**
  14 + * {@link TimestampLocalDateTimeDeserializer} 的单元测试
  15 + *
  16 + * 覆盖全部在用格式(epoch 毫秒 / 常见字符串 / ISO / 纯日期),
  17 + * 以及非法输入抛清晰错误(不再静默落 1970-01-01)。
  18 + *
  19 + * @author 芋道源码
  20 + */
  21 +public class TimestampLocalDateTimeDeserializerTest {
  22 +
  23 + private static final ObjectMapper MAPPER;
  24 +
  25 + static {
  26 + ObjectMapper mapper = new ObjectMapper();
  27 + SimpleModule module = new SimpleModule();
  28 + module.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
  29 + mapper.registerModule(module);
  30 + MAPPER = mapper;
  31 + }
  32 +
  33 + private LocalDateTime parse(String json) throws Exception {
  34 + return MAPPER.readValue(json, LocalDateTime.class);
  35 + }
  36 +
  37 + @Test
  38 + public void testEpochMilliNumber() throws Exception {
  39 + // 1700000000000 = 2023-11-14T22:13:20Z(本地时区换算后)
  40 + LocalDateTime expected = LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(1700000000000L), java.time.ZoneId.systemDefault());
  41 + assertEquals(expected, parse("1700000000000"));
  42 + }
  43 +
  44 + @Test
  45 + public void testEpochMilliString() throws Exception {
  46 + LocalDateTime expected = LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(1700000000000L), java.time.ZoneId.systemDefault());
  47 + assertEquals(expected, parse("\"1700000000000\""));
  48 + }
  49 +
  50 + @Test
  51 + public void testSpaceFormat() throws Exception {
  52 + assertEquals(LocalDateTime.of(2026, 9, 11, 8, 0, 0), parse("\"2026-09-11 08:00:00\""));
  53 + }
  54 +
  55 + @Test
  56 + public void testSpaceFormatWithMillis() throws Exception {
  57 + assertEquals(LocalDateTime.of(2026, 9, 11, 8, 0, 0, 123000000), parse("\"2026-09-11 08:00:00.123\""));
  58 + }
  59 +
  60 + @Test
  61 + public void testIsoLocal() throws Exception {
  62 + assertEquals(LocalDateTime.of(2026, 9, 11, 8, 0, 0), parse("\"2026-09-11T08:00:00\""));
  63 + }
  64 +
  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
  72 + public void testIsoWithOffset() throws Exception {
  73 + // 前端 Date 对象序列化:带 Z(UTC)。2026-09-11T00:00:00Z 换算到系统时区
  74 + LocalDateTime expected = java.time.OffsetDateTime.parse("2026-09-11T00:00:00Z")
  75 + .atZoneSameInstant(java.time.ZoneId.systemDefault()).toLocalDateTime();
  76 + assertEquals(expected, parse("\"2026-09-11T00:00:00Z\""));
  77 + }
  78 +
  79 + @Test
  80 + public void testIsoWithOffsetMillis() throws Exception {
  81 + // JSON.stringify(Date) 实际产物:带毫秒 + Z(无 value-format 的 Date 对象控件提交格式)
  82 + LocalDateTime expected = java.time.OffsetDateTime.parse("2026-09-11T00:00:00.000Z")
  83 + .atZoneSameInstant(java.time.ZoneId.systemDefault()).toLocalDateTime();
  84 + assertEquals(expected, parse("\"2026-09-11T00:00:00.000Z\""));
  85 + }
  86 +
  87 + @Test
  88 + public void testIsoWithOffsetPlus() throws Exception {
  89 + // 带 +08:00 时区偏移的形式
  90 + LocalDateTime expected = java.time.OffsetDateTime.parse("2026-09-11T08:00:00.000+08:00")
  91 + .atZoneSameInstant(java.time.ZoneId.systemDefault()).toLocalDateTime();
  92 + assertEquals(expected, parse("\"2026-09-11T08:00:00.000+08:00\""));
  93 + }
  94 +
  95 + @Test
  96 + public void testDateOnly() throws Exception {
  97 + assertEquals(LocalDateTime.of(2026, 9, 11, 0, 0, 0), parse("\"2026-09-11\""));
  98 + }
  99 +
  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
  123 + public void testNullAndEmpty() throws Exception {
  124 + assertNull(parse("null"));
  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\""));
  133 + }
  134 +
  135 + @Test
  136 + public void testInvalidThrows() {
  137 + // 非法输入抛清晰错误,不再静默落 1970
  138 + assertThrows(Exception.class, () -> parse("\"not-a-date\""));
  139 +
  140 + }
  141 +
  142 +}