gorm分页时区问题根源在于未显式控制时区上下文:mysql与go时区须对齐,dsn需指定loc=utc,created_at写入与解析均需统一用utc,游标分页须用where created_at > ? order by created_at asc, id asc,并避免date()等索引失效操作。

GORM 本身不感知时区,分页逻辑不会自动适配跨国场景——所有“时间相关分页”出问题,根源都在你没显式控制时区上下文。
用 created_at 分页时,数据库时区和 Go 应用时区必须对齐
MySQL 默认用系统时区(如 +08:00),而 Go 的 time.Now() 默认用本地时区(可能为 UTC 或 +08:00),一旦不一致,ORDER BY created_at DESC 的结果就会漂移:同一条记录在不同时区下排序位置不同,翻页时漏数据或重复出现。
- 检查 MySQL 当前时区:
SELECT @@global.time_zone, @@session.time_zone; - Go 启动时强制设为 UTC:
os.Setenv("TZ", "UTC"),或初始化db前调用time.LoadLocation("UTC") - 连接 DSN 中显式指定时区:
parseTime=true&loc=UTC(MySQL)或timezone=utc(PostgreSQL) - 所有写入
created_at的字段,必须用统一时区的time.Time值,别依赖数据库默认值
游标分页传 last_created_at,必须带时区信息
前端 JavaScript 的 new Date().toISOString() 返回带 Z 的 UTC 时间,但若后端直接用 time.Parse("2006-01-02...", s) 解析,会按本地时区解释,导致游标错位。正确做法是:
- 前端传 ISO 格式字符串(如
"2026-08-21T06:15:22.123Z"),后端用time.Parse(time.RFC3339, s)解析,它能自动识别 Z 和 ±hh:mm - 若前端只能传无时区时间(如
"2026-08-21 14:15:22"),必须约定时区(如全部按 UTC),并在解析时显式绑定:time.ParseInLocation("2006-01-02 15:04:05", s, time.UTC) - 游标查询 SQL 必须写成:
WHERE created_at > ? ORDER BY created_at ASC, id ASC(注意:ASC + id ASC 是防时间重复的刚需)
Count 查询不能依赖 WHERE created_at BETWEEN,要转成 UTC 时间范围
当业务需统计“今天全球各时区用户注册数”,若直接用 WHERE DATE(created_at) = CURDATE(),MySQL 会按服务器时区算“今天”,结果对海外用户失真。应改为:
- 将“今天”换算为 UTC 时间范围:
start := time.Now().In(time.UTC).Truncate(24 <em> time.Hour)</em>,end := start.Add(24 time.Hour) - 查询写成:
WHERE created_at >= ? AND created_at ,参数传 <code>start和end的time.Time值(GORM 自动处理时区转换) - 避免在
Count()中用函数包裹字段(如DATE(created_at)),会导致索引失效;用范围查询才能走created_at索引
跨时区分页最易被忽略的点,不是语法,而是时区隐含状态——哪怕只有一处没对齐(比如数据库用 +08:00、Go 用本地、前端传时间没带 Z),就足以让第 3 页开始的数据乱序。游标分页比 Offset 更容错,但前提是时间字段全程在同一个时区语义下流转。











