Abel'Blog

我干了什么?究竟拿了时间换了什么?

0%

gorm与postgreSQL里面time

概述

最近项目里面的按照时区来计算的时候存在少计算了8小时的问题;

timestamptz 是 PostgreSQL 中的一种时间/日期类型,完整名称是 timestamp with time zone。它和普通的 timestamp(即 timestamp without time zone)有一些区别:

timestamptz 带时区的时间戳(推荐)PostgreSQL 内部会存储为 UTC 时间;查询时会根据你的 会话时区(TimeZone 配置) 自动转换显示;

timestamp:不带时区;只存储一个绝对时间,不做时区转换;

原因分析

  1. PostgreSQL 内部存储 UTC

    • timestamptz 实际存储的是 UTC 时间

    • 比如你在东八区插入 ‘2025-10-21 00:00:00’,数据库会存储为 ‘2025-10-20 16:00:00+00’

  2. GORM / Go 的 time.Time 默认

• Go 的 time.Time 有 Location 信息

• 如果直接用 time.Date(2025,10,21,0,0,0,0,time.UTC),查出来会跟本地时区偏 8 小时

• 如果数据库/Go 会话时区不一致,会导致查询 WHERE submit_time >= ‘今天 0 点’ 时,结果偏移

解决方案 a

方法 A:在 Go 中使用本地时区创建时间

1
2
3
4
5
6
7
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八区
today := time.Now().In(loc)
startOfDay := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, loc)

// GORM 查询
var rows []CrossChainTx
db.Where("submit_time >= ?", startOfDay).Find(&rows)

封装起来的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// GetCSTDayRange 返回指定日期(向前推 i 天)的东八区 0 点到24点时间段
func GetCSTDayRange(daysAgo int) (start, end time.Time) {
// 加载东八区时区
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
// 加载失败回退到 UTC
loc = time.FixedZone("CST", 8*3600)
}

// 当前时间 - i 天
now := time.Now().In(loc).AddDate(0, 0, -daysAgo)

// 当天东八区 0 点
start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)

// 当天结束时间
end = start.Add(24 * time.Hour)
return
}

// GetCSTTodayRange 返回今天东八区 0 点到24点
func GetCSTTodayRange() (start, end time.Time) {
return GetCSTDayRange(0)
}

解决方案 b

在 SQL 里使用 PostgreSQL 函数处理

•    PostgreSQL 可以在查询里直接按东八区截断日期:
1
2
3
SELECT *
FROM cross_chain_tx
WHERE submit_time >= date_trunc('day', now() AT TIME ZONE 'Asia/Shanghai');
•    date_trunc('day', ...) 截断到当地 0 点
•    AT TIME ZONE 'Asia/Shanghai' 保证是东八区

在 GORM 中:

1
2
3
4
5
db.Raw(`
SELECT *
FROM cross_chain_tx
WHERE submit_time >= date_trunc('day', now() AT TIME ZONE 'Asia/Shanghai')
`).Scan(&rows)

方法 C:统一使用 UTC 处理

•    服务器和数据库都统一用 UTC
•    在 Go 中处理时间显示时,再转换到东八区
•    优点:逻辑简单,跨时区可靠
1
2
3
utcNow := time.Now().UTC()
startOfDayUTC := time.Date(utcNow.Year(), utcNow.Month(), utcNow.Day(), 0, 0, 0, 0, time.UTC)
db.Where("submit_time >= ?", startOfDayUTC).Find(&rows)
•    但是显示给用户时,需要 .In(loc) 转成东八区

实务推荐:如果只是国内服务,使用 方法 A(Go 本地时区) 最方便。
如果有多时区用户,统一用 UTC + 显示转换更安全。

附带

c++ 版本

1
2
auto tz = std::chrono::locate_zone("Asia/Shanghai");
auto now = std::chrono::zoned_time(tz, std::chrono::system_clock::now());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <ctime>
#include <chrono>

// 获取东八区(CST)当天 0 点的 UTC 时间点
std::time_t getTodayStartCST() {
using namespace std::chrono;

// 当前 UTC 时间
system_clock::time_point now = system_clock::now();

// 转为 time_t
std::time_t t_now = system_clock::to_time_t(now);

// 转为 struct tm (UTC)
std::tm utc_tm = *std::gmtime(&t_now);

// 计算今天 UTC 0 点时间戳
utc_tm.tm_hour = 0;
utc_tm.tm_min = 0;
utc_tm.tm_sec = 0;
std::time_t utc_midnight = timegm(&utc_tm);

// 东八区 0 点比 UTC 早 8 小时,因此减 8 小时得到 UTC 表示的东八区 0 点
const int CST_OFFSET = 8 * 3600;
std::time_t cst_midnight = utc_midnight - CST_OFFSET;

return cst_midnight;
}

int main() {
std::time_t start_cst = getTodayStartCST();

// 打印东八区当天 0 点(本地时间)
std::cout << "CST 0点 (UTC时间): " << std::asctime(std::gmtime(&start_cst));

// 如果你想以东八区形式显示
std::time_t display = start_cst + 8 * 3600;
std::cout << "CST 0点 (北京时间): " << std::asctime(std::gmtime(&display));

return 0;
}

c 语言版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <time.h>

// 获取东八区当天 0 点的 UTC 时间戳
time_t get_today_start_cst() {
time_t now;
struct tm *utc_tm;
struct tm tm_copy;
const int CST_OFFSET = 8 * 3600; // 东八区偏移(秒)

// 当前时间(UTC)
time(&now);
utc_tm = gmtime(&now); // 转为UTC时间结构
tm_copy = *utc_tm; // 拷贝一份,因为 mktime/localtime 可能修改原结构

// 将时分秒清零(UTC的当天 0 点)
tm_copy.tm_hour = 0;
tm_copy.tm_min = 0;
tm_copy.tm_sec = 0;

// UTC 0点时间戳
time_t utc_midnight = timegm(&tm_copy);

// 东八区 0 点比 UTC 提前 8 小时 → UTC时间要 -8h
time_t cst_midnight = utc_midnight - CST_OFFSET;

return cst_midnight;
}

int main() {
time_t start_cst = get_today_start_cst();

printf("CST 当天 0 点 (UTC时间): %s", asctime(gmtime(&start_cst)));

// 转成北京时间打印
time_t beijing_time = start_cst + 8 * 3600;
printf("CST 当天 0 点 (北京时间): %s", asctime(gmtime(&beijing_time)));

return 0;
}

rust 版本

Cargo.toml

1
2
[dependencies]
chrono = "0.4"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use chrono::{Local, TimeZone, Timelike, Datelike, FixedOffset, Duration};

/// 获取东八区当天 0 点和 24 点(CST: Asia/Shanghai)
fn get_today_cst_range() -> (chrono::DateTime<FixedOffset>, chrono::DateTime<FixedOffset>) {
// 定义东八区偏移
let cst = FixedOffset::east_opt(8 * 3600).unwrap();

// 获取当前时间并转换为东八区
let now_cst = Local::now().with_timezone(&cst);

// 获取当天 0 点
let start_of_day = cst
.with_ymd_and_hms(now_cst.year(), now_cst.month(), now_cst.day(), 0, 0, 0)
.unwrap();

// 获取当天 24 点
let end_of_day = start_of_day + Duration::hours(24);

(start_of_day, end_of_day)
}

fn main() {
let (start, end) = get_today_cst_range();

println!("🕛 东八区当天 0 点: {}", start);
println!("🕕 东八区当天 24 点: {}", end);
}