diff --git a/daxpay-platform/daxpay-platform-common/common-json/src/main/java/cn/daxpay/open/platform/common/json/configuration/JacksonConfiguration.java b/daxpay-platform/daxpay-platform-common/common-json/src/main/java/cn/daxpay/open/platform/common/json/configuration/JacksonConfiguration.java index c4aa98fcd..9bbcc2697 100644 --- a/daxpay-platform/daxpay-platform-common/common-json/src/main/java/cn/daxpay/open/platform/common/json/configuration/JacksonConfiguration.java +++ b/daxpay-platform/daxpay-platform-common/common-json/src/main/java/cn/daxpay/open/platform/common/json/configuration/JacksonConfiguration.java @@ -4,6 +4,7 @@ import jakarta.annotation.PostConstruct; import cn.daxpay.open.platform.common.json.jdk.Java8TimeFormatModule; import cn.daxpay.open.platform.common.json.jdk.JavaLongTypeModule; import cn.daxpay.open.platform.common.json.util.JacksonUtil; +import cn.daxpay.open.platform.common.json.util.OffsetDateTimeParseUtil; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.PropertyAccessor; @@ -11,15 +12,18 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.SerializationFeature; +import java.time.OffsetDateTime; import java.util.TimeZone; -/// # jackson 序列化自动配置 +/// # jackson 序列化与时间参数自动配置 /// -/// 使用 Spring Boot 4 标准的 JsonMapperBuilderCustomizer 定制机制, 不手动创建 ObjectMapper +/// 使用 Spring Boot 4 标准的 JsonMapperBuilderCustomizer 定制机制, 不手动创建 ObjectMapper. +/// 另注册 OffsetDateTime 的 query/form 参数 [Converter], 使前端时间范围无时区字符串可绑定 (见 [#offsetDateTimeConverter]) @Configuration public class JacksonConfiguration { @@ -64,4 +68,12 @@ public class JacksonConfiguration { return copy; } -} \ No newline at end of file + /// OffsetDateTime query/form 参数转换 (Spring ConversionService, 非 jackson) + /// 前端时间范围选择器提交 `yyyy-MM-dd HH:mm:ss` 无时区字符串, 默认无法绑定 OffsetDateTime, + /// 复用 [OffsetDateTimeParseUtil] 与 JSON 反序列化保持两条绑定路径解析一致 + @Bean + Converter offsetDateTimeConverter() { + return OffsetDateTimeParseUtil::parse; + } + +} diff --git a/daxpay-platform/daxpay-platform-common/common-json/src/main/java/cn/daxpay/open/platform/common/json/deserializer/DaxpayOffsetDateTimeDeserializer.java b/daxpay-platform/daxpay-platform-common/common-json/src/main/java/cn/daxpay/open/platform/common/json/deserializer/DaxpayOffsetDateTimeDeserializer.java index 812cf4879..1c9e60fe7 100644 --- a/daxpay-platform/daxpay-platform-common/common-json/src/main/java/cn/daxpay/open/platform/common/json/deserializer/DaxpayOffsetDateTimeDeserializer.java +++ b/daxpay-platform/daxpay-platform-common/common-json/src/main/java/cn/daxpay/open/platform/common/json/deserializer/DaxpayOffsetDateTimeDeserializer.java @@ -1,45 +1,19 @@ package cn.daxpay.open.platform.common.json.deserializer; +import cn.daxpay.open.platform.common.json.util.OffsetDateTimeParseUtil; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; import tools.jackson.databind.DeserializationContext; import tools.jackson.databind.ValueDeserializer; -import java.time.LocalDateTime; import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -/// # OffsetDateTime 反序列化器, 支持三种格式: -/// ## - ISO 8601 带偏移: 2026-06-13T08:30:00Z 或 2026-06-13T16:30:00+08:00 -/// ## - 旧格式: 2026-06-13 16:30:00 (按 Asia/Shanghai 时区解析后转 UTC) -/// ## - 旧格式毫秒: 2026-06-13 16:30:00.123 -/// +/// # OffsetDateTime 反序列化器 +/// 解析逻辑详见 [OffsetDateTimeParseUtil], 支持三种格式: ISO 带偏移 / `yyyy-MM-dd HH:mm:ss` / `yyyy-MM-dd HH:mm:ss.SSS` public class DaxpayOffsetDateTimeDeserializer extends ValueDeserializer { - private static final DateTimeFormatter FORMATTER_OLD = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - private static final DateTimeFormatter FORMATTER_OLD_MS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); - private static final ZoneOffset SHANGHAI_OFFSET = ZoneOffset.ofHours(8); - @Override public OffsetDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { - String text = p.getString(); - if (text == null || text.isBlank()) { - return null; - } - try { - return OffsetDateTime.parse(text); - } catch (Exception ignored) { - } - try { - if (text.contains(".")) { - LocalDateTime localDateTime = LocalDateTime.parse(text, FORMATTER_OLD_MS); - return localDateTime.atOffset(SHANGHAI_OFFSET).withOffsetSameInstant(ZoneOffset.UTC); - } - LocalDateTime localDateTime = LocalDateTime.parse(text, FORMATTER_OLD); - return localDateTime.atOffset(SHANGHAI_OFFSET).withOffsetSameInstant(ZoneOffset.UTC); - } catch (Exception e) { - throw new RuntimeException("无法解析日期时间: " + text, e); - } + return OffsetDateTimeParseUtil.parse(p.getString()); } } diff --git a/daxpay-platform/daxpay-platform-common/common-json/src/main/java/cn/daxpay/open/platform/common/json/util/OffsetDateTimeParseUtil.java b/daxpay-platform/daxpay-platform-common/common-json/src/main/java/cn/daxpay/open/platform/common/json/util/OffsetDateTimeParseUtil.java new file mode 100644 index 000000000..d5b748802 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-json/src/main/java/cn/daxpay/open/platform/common/json/util/OffsetDateTimeParseUtil.java @@ -0,0 +1,48 @@ +package cn.daxpay.open.platform.common.json.util; + +import lombok.experimental.UtilityClass; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +/// # OffsetDateTime 字符串解析工具 +/// 供 Jackson 反序列化器与 Spring query/form 参数 [Converter] 共用, +/// 保证 JSON body 与 GET query / form 表单两条绑定路径的解析行为一致. +/// +/// 支持三种格式: +/// ## - ISO 8601 带偏移: 2026-06-13T08:30:00Z 或 2026-06-13T16:30:00+08:00 +/// ## - 旧格式: 2026-06-13 16:30:00 (按东八区解析后转 UTC) +/// ## - 旧格式毫秒: 2026-06-13 16:30:00.123 +@UtilityClass +public class OffsetDateTimeParseUtil { + + private static final DateTimeFormatter FORMATTER_OLD = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private static final DateTimeFormatter FORMATTER_OLD_MS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + private static final ZoneOffset SHANGHAI_OFFSET = ZoneOffset.ofHours(8); + + /// 将字符串解析为 OffsetDateTime + public OffsetDateTime parse(String text) { + if (text == null || text.isBlank()) { + return null; + } + // 优先按 ISO 带偏移解析 + try { + return OffsetDateTime.parse(text); + } catch (Exception ignored) { + } + // 回退: 旧格式(无时区字面量), 先用 LocalDateTime 接住, 再附加东八区偏移转为 UTC + try { + DateTimeFormatter formatter = text.contains(".") + ? FORMATTER_OLD_MS + : FORMATTER_OLD; + LocalDateTime localDateTime = LocalDateTime.parse(text, formatter); + return localDateTime.atOffset(SHANGHAI_OFFSET).withOffsetSameInstant(ZoneOffset.UTC); + } catch (Exception e) { + throw new RuntimeException("无法解析日期时间: " + text, e); + } + } +}