通用时区时间工具类

客户1:嗨,这有个bug,我查当天的数据只能查到8点之后的,8点之前的没有
操作1:发现数据库存的数据对不上 少了八个小时,于是让ai开始改后端改sql,让数据库存正确的时间,更新!

客户2:原本9:00的数据在展示页变成了17:00
操作2:又让ai埋头苦干改前端

也不知道ai在叽里咕噜说什么,但是出现次数最多的是“时区”导致的问题

UTC时区 (协调世界时)

UTC时区是全球统一的时间标准,不受时区和夏令时影响,是所有时区计算的基准,如北京时间为 UTC+8,纽约时间为 UTC-4/UTC-5 (夏令时),服务器和数据库应统一使用 UTC 存储时间,避免地域差异导致的混乱

  • 时间示例:2026-03-31 12:00:00 UTC

  • ISO 8601 简写:Z(最常用!)
    例:2026-03-31T12:00:00Z → 代表UTC 时间

  • 固定偏移量时区

    格式:

    1. UTC±HH
    2. UTC±HH:MM
    3. ±HH:MM(ISO 标准)
    4. ±HHMM

    例如:
    中国:UTC+8、+08:00、+0800
    纽约冬令时:UTC-5、-05:00
    纽约夏令时:UTC-4、-04:00
    伦敦冬令时:UTC+0、±00:00

    ISO 时间示例:
    2026-03-31T20:00:00+08:00

也就是说UTC表示的时间会比北京时间少8个小时,所以导致客户1反馈的问题,而我的操作1将数据库中存的UTC时间变成了北京时间(这其实是不对的,如果软件全球化会导致时间混乱,所以数据库中应该存UTC时区的时间)

数据库查询

mysql中可以通过CONVERT_TZ将UTC → 北京时间
例如

---sql1  ✅ 安全:函数作用在查询结果,索引有效SELECT 
  CONVERT_TZ(create_time, '+00:00', '+08:00') AS beijing_time 
FROM table 
WHERE create_time >= '2026-04-01 00:00:00'; -- 条件用UTC时间

--- sql2 ❌ 致命:函数作用在索引字段,索引失效 → 慢SQL
SELECT * FROM table
WHERE CONVERT_TZ(create_time, '+00:00', '+08:00') = '2026-04-01 17:00:00';

我们知道,当条件查询时,查询条件使用函数会导致索引失效,so? sql2 这段代码完全喊天菩萨!正确的做法应该在参数传入之前就把时间转好(这是工具类需要实现的功能点

前端展示

在没有进行bug修复之前,由于错错得对,虽然查询时间有问题,但是界面展示是没有问题的,原因是React进行了时区转换,React 本身没有任何时区设置,页面显示时间完全由 浏览器的 JavaScript 引擎决定;

后端(Java)存 UTC 时间(比如 2026-03-31T12:00:00Z,末尾 Z = UTC)

传给前端的是 UTC 时间戳 / UTC 字符串

前端用 new Date(utc时间) 解析时,JS 会自动将 UTC 时间 → 转换为「用户设备的本地时区」时间

本地时区 = 用户设备(电脑 / 手机)操作系统设置的时区
在这里插入图片描述

关键配置

  1. JDBC 连接串配置中serverTimezone=UTC
  2. SpringBoot 配置中
spring:
  timezone: UTC
  jackson:
    time-zone: UTC  # JSON序列化统一UTC

工具类

综上,让ai埋头苦干写个工具类

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Date;

/**
 * 全局统一时间工具类
 * 规范:
 * 1. 数据库存储 = UTC Instant
 * 2. 业务逻辑 = 北京时间(Asia/Shanghai)
 * 3. 查询MySQL:一律用【北京时间转UTC】,WHERE条件绝不使用CONVERT_TZ
 * 4. 全程不依赖JVM默认时区
 */
public final class DateTimeUtil {

    // ===================== 1. 固定时区常量(全局唯一,不许改) =====================
    public static final ZoneId ZONE_UTC = ZoneId.of("UTC");
    public static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");

    // 标准格式化(不带时区,业务展示用)
    public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    public static final DateTimeFormatter FORMATTER_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    private DateTimeUtil() {}

    // ===================== 2. 获取当前时间(强制指定时区,禁止用原生XXX.now()) =====================

    /**
     * 当前UTC时间戳 → 存入数据库、MySQL、Redis
     */
    public static Instant nowUtcInstant() {
        return Instant.now();
    }

    /**
     * 当前北京时间(带时区)
     */
    public static ZonedDateTime nowShanghai() {
        return ZonedDateTime.now(ZONE_SHANGHAI);
    }

    /**
     * 当前北京时间(本地时间,无时区信息,仅业务展示)
     */
    public static LocalDateTime nowShanghaiLocal() {
        return LocalDateTime.now(ZONE_SHANGHAI);
    }

    // ===================== 3. 核心:北京时间 ↔ UTC 互转(最重要!) =====================

    /**
     * 北京 LocalDateTime → UTC Instant(用于存库、MySQL查询)
     */
    public static Instant shanghaiToUtc(LocalDateTime shanghaiLocal) {
        return shanghaiLocal.atZone(ZONE_SHANGHAI).toInstant();
    }

    /**
     * UTC Instant → 北京 LocalDateTime(用于前端展示、业务逻辑)
     */
    public static LocalDateTime utcToShanghai(Instant utcInstant) {
        return LocalDateTime.ofInstant(utcInstant, ZONE_SHANGHAI);
    }

    // ===================== 4. 日期范围:北京某天 开始/结束 转UTC(解决慢SQL!) =====================

    /**
     * 北京日期(yyyy-MM-dd)→ 当天 00:00:00 的UTC时间
     * 用于 WHERE create_time >= ?
     */
    public static Instant shanghaiDayStart(String yyyyMMdd) {
        LocalDate localDate = LocalDate.parse(yyyyMMdd, FORMATTER_DATE);
        LocalDateTime start = LocalDateTime.of(localDate, LocalTime.MIN);
        return shanghaiToUtc(start);
    }

    /**
     * 北京日期(yyyy-MM-dd)→ 当天 23:59:59.999 的UTC时间
     * 用于 WHERE create_time <= ?
     */
    public static Instant shanghaiDayEnd(String yyyyMMdd) {
        LocalDate localDate = LocalDate.parse(yyyyMMdd, FORMATTER_DATE);
        LocalDateTime end = LocalDateTime.of(localDate, LocalTime.MAX);
        return shanghaiToUtc(end);
    }

    // ===================== 5. 字符串解析/格式化(带时区,绝对安全) =====================

    /**
     * 解析北京时间字符串 → Instant(UTC)
     */
    public static Instant parseShanghaiToUtc(String timeStr) {
        LocalDateTime local = LocalDateTime.parse(timeStr, FORMATTER);
        return shanghaiToUtc(local);
    }

    /**
     * UTC时间 → 北京时间字符串
     */
    public static String formatUtcToShanghai(Instant utcInstant) {
        return utcToShanghai(utcInstant).format(FORMATTER);
    }

    // ===================== 6. 旧 Date 兼容 =====================

    public static Instant dateToUtc(Date date) {
        return date.toInstant();
    }

    public static Date utcToDate(Instant instant) {
        return Date.from(instant);
    }

}
  • 存库 Instant now = DateTimeUtil.nowUtcInstant();
  • 前端显示 LocalDateTime shanghaiTime = DateTimeUtil.utcToShanghai(utcInstant);(React 拿到后不会多 + 8、不会少 8)
  • 查询全天
    Instant start = DateTimeUtil.shanghaiDayStart(“2026-04-01”);
    Instant end = DateTimeUtil.shanghaiDayEnd(“2026-04-01”);
时间类 now () 方法默认时区 携带时区信息? 核心用途
Instant UTC 存储、计算、基准时间
LocalDateTime/LocalDate/LocalTime JVM 默认时区 纯本地展示(无时区)
ZonedDateTime JVM 默认时区 完整时区处理、夏令时

数据库字段

  • TIMESTAMP:MySQL 自动转 UTC 存储,推荐
  • DATETIME:纯文本存储,需手动保证存 UTC
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐