main.go 原本在启动时执行 time.Local = timeutil.Location(),靠隐式改写进程全局 状态把时间钉在 +08:00。副作用是任何跑在这行之前的代码都处于另一个时区,且依赖 关系完全不可见。 - 移除该全局赋值,业务时间全部走 timeutil 的显式时区 - rock_snapshot.parseRockTime 是唯一真实依赖,且是会出事的那一处:上游返回不带 偏移的北京时间墙面时间,用 time.Local 解析会在 TZ≠+08:00 时整体偏移 8 小时, 把「本次新绑定」误判为「历史残留」而放回库存,直接造成重复发货。 改为 timeutil.Location() 并补回归测试 - 有一点无法绕开:pgx 会把 timestamptz 解码到 time.Local,所以接口 JSON 的时间 偏移仍由进程时区决定。新增 timeutil.LocalZoneNotice(),启动时把不一致显式告警, 交由运维用 TZ=Asia/Shanghai 声明(Docker 部署已内置),而不是靠代码偷偷改全局 - 测试时间基线改用业务时区,避免随宿主机 TZ 漂移 验证:全量测试在默认时区、TZ=UTC、TZ=America/New_York 三种环境下均通过; 实测 TZ=UTC 启动打印时区提示,业务时区下静默。
59 lines
1.9 KiB
Go
59 lines
1.9 KiB
Go
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)
|
||
}
|