package timeutil import ( "fmt" "time" ) const ( APITimeLayout = "2006-01-02T15:04:05-07:00" OrderNoLayout = "20060102150405" // BusinessZoneOffsetSeconds 是业务时区(北京时间)相对 UTC 的偏移秒数。 BusinessZoneOffsetSeconds = 8 * 60 * 60 ) var businessLocation = time.FixedZone("CST", BusinessZoneOffsetSeconds) func Location() *time.Location { return businessLocation } // LocalZoneNotice 在进程本地时区与业务时区不一致时返回提示文本,一致时返回空串。 // // 为什么需要这个检查:pgx 会把 Postgres 的 timestamptz 解码到 time.Local, // 因此接口 JSON 里时间的偏移量由「进程时区」决定。对外契约要求统一为北京时间 // (见 docs/API对接关系.md),Docker 部署靠 TZ=Asia/Shanghai 满足它。 // // 历史上这里用 `time.Local = timeutil.Location()` 隐式改写全局变量来达到目的, // 副作用是任何读到那行之前的代码都可能在另一个时区下运行。现在改为在启动时 // 把不一致显式暴露出来,由运维通过 TZ 显式声明,而不是靠代码偷偷改全局状态。 func LocalZoneNotice() string { name, offset := time.Now().Zone() if offset == BusinessZoneOffsetSeconds { return "" } return fmt.Sprintf( "进程本地时区为 %s(UTC%+d),与业务时区 UTC+8 不一致。pgx 会把 timestamptz 解码到 time.Local,"+ "因此接口返回的时间偏移量会随之变化;请显式设置 TZ=Asia/Shanghai(Docker 部署已内置)。", name, offset/3600, ) } func Now() time.Time { return time.Now().In(businessLocation) } func FormatAPITime(value time.Time) string { if value.IsZero() { return "" } return value.In(businessLocation).Truncate(time.Second).Format(APITimeLayout) } func StartOfDay(value time.Time) time.Time { local := value.In(businessLocation) year, month, day := local.Date() return time.Date(year, month, day, 0, 0, 0, 0, businessLocation) }