前后端统一UTC时区指南

854 阅读3分钟

这几年经手了很多面向全球用户的Web项目,每个项目对时区处理都不大一样,Date和Time的类型也不太一样,所以来做个总结。

本文将介绍一套最简单的实现,能满足绝大多数项目的时区需求:后端统一用UTC时区(0时区)来落库、计算、触发器,前端根据用户浏览器时区来渲染。

后端

实体

Java8之后,就不要再使用java.sql.*java.util.Date了,而是使用java.time.*包下的类型:LocalTime、 LocalDate和LocalDateTime:

@Data
@Entity
@Table(name = "date_time_record")
public class DateTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private LocalDate localDate; // map with pg date type
    
    private LocalTime localTime; // map with pg time type
    
    private LocalDateTime localDateTime; // map with pg timestamp type
}

数据库

以PostgreSQL为例,可以将类型TIME、DATE和TIMESTAMP映射到java.time类型

create table public.date_time_record
(
    id              bigserial primary key,
    local_date      date,         -- 无时区的日期类型(2024-10-01)
    local_time      time(6),      -- 无时区的日期类型(12:34:56)
    local_date_time timestamp(6)  -- 无时区的日期+时间类型(2024-09-13 06:30:05.971952)
);

Jackson序列化和反序列化

通过@JsonFormat注解来指定日期格式的转换规则

@Data
public class DateTimeDTO {

    @Schema(example = "2024-10-01") // swagger的包
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    private LocalDate localDate;

    @Schema(example = "12:34:56")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm:ss")
    private LocalTime localTime;

    @Schema(example = "2024-10-01T12:34:56", type = "string")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-d'T'HH:mm:ss")
    private LocalDateTime localDateTime;
}

Controller入参绑定

@RestController
@RequestMapping("/api")
public class DateTimeController {

    @Resource
    private DateTimeRepository dateTimeRepository;

    @PostMapping("/datetime")
    public ResponseEntity<DateTimeDTO> save(@RequestBody DateTimeDTO dto) {
        DateTimeEntity entity = new DateTimeEntity();
        BeanUtils.copyProperties(dto, entity);

        dateTimeRepository.save(entity);

        return ResponseEntity.ok(dto);
    }
}

全局UTC时区

我们在代码中需要使用LocalDate.now()LocalDateTime.now()等API时,会使用本地时区如UTC+8,所以需要全局配置为UTC时区,以避免计算和落库时发生时区问题。

@SpringBootApplication
public class DateTimeBindingDemoApplication {

    public static void main(String[] args) {

        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));

        SpringApplication.run(DateTimeBindingDemoApplication.class, args);
    }
}

至此,API的入参绑定、系统内计算、落库、出参Json格式都已得到适配

image.png

前端

LocalDateTime在前端做转换是没问题的,但由于LocalDate和LocalTime缺少用来offset的部分,则需要补全为一个完整的LocalDateTime才可以做转换(解释:例如2024-10-01,在中国时间7:59时,UTC还是2024-09-30;所以为了信息完整,最好用LocalDateTime类型)

前端如果有针对不同时区进行渲染的需求,有一系列很好用的的API:

示例:

// 从后端获取的日期时间字符串+Z表示UTC时区,也可以直接让后端返回TZ格式
const localDateTimeString = "2024-10-01T12:34:56" + "Z";

// 将TZ格式的字符串转换为Date对象
const utcDateTime = new Date(localDateTimeString);

// toLocaleString默认会使用浏览器时区,输出为:2024/10/1 20:34:56
console.log(utcDateTime.toLocaleString());

// 也可以添加不同的locale,这个参数并不影响timezone 
// 输出为:10/1/2024, 8:34:56 PM
console.log(utcDateTime.toLocaleString('en-US'));
// 输出为:2024/10/1 20:34:56
console.log(utcDateTime.toLocaleString('zh-CN'));

// 也可以添加参数,比如timezone和format格式,更详细的参考MDN文档
// 输出为:October 1, 2024 at 08:34:56 AM
console.log(utcDateTime.toLocaleString('en-US', { timeZone: 'America/New_York', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true }));