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 启动打印时区提示,业务时区下静默。
62 lines
1.7 KiB
Go
62 lines
1.7 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestParseRockTimeUsesBusinessZone 锁定「上游不带偏移的时间串必须按北京时间解析」。
|
|
//
|
|
// 回归背景:上游返回的是不带偏移的北京时间墙面时间,用 time.Local 解析等于假设
|
|
// 进程时区就是北京时间。一旦 TZ≠+08:00,解析出的瞬时值就会整体偏移
|
|
// (UTC 下差 8 小时),而绑定时序判断依赖它来区分「历史残留」与「本次新绑定」——
|
|
// 误判会把刚扫码绑定的 Token 放回库存,直接造成重复发货。
|
|
func TestParseRockTimeUsesBusinessZone(t *testing.T) {
|
|
original := time.Local
|
|
time.Local = time.UTC
|
|
t.Cleanup(func() { time.Local = original })
|
|
|
|
cases := []struct {
|
|
name string
|
|
raw string
|
|
want time.Time
|
|
}{
|
|
{
|
|
name: "空格分隔的北京时间按 +08:00 解析",
|
|
raw: "2026-10-05 20:32:17",
|
|
want: time.Date(2026, 10, 5, 12, 32, 17, 0, time.UTC),
|
|
},
|
|
{
|
|
name: "T 分隔且无偏移",
|
|
raw: "2026-09-27T14:38:51.23",
|
|
want: time.Date(2026, 9, 27, 6, 38, 51, 230000000, time.UTC),
|
|
},
|
|
{
|
|
name: "自带偏移的时间串不受进程时区影响",
|
|
raw: "2026-10-05T20:32:17Z",
|
|
want: time.Date(2026, 10, 5, 20, 32, 17, 0, time.UTC),
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, ok := parseRockTime(tc.raw)
|
|
if !ok {
|
|
t.Fatalf("解析 %q 失败", tc.raw)
|
|
}
|
|
if !got.Equal(tc.want) {
|
|
t.Fatalf("解析 %q 期望 %s,实际 %s", tc.raw, tc.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestParseRockTimeRejectsEmpty 空串与空白必须解析失败。
|
|
func TestParseRockTimeRejectsEmpty(t *testing.T) {
|
|
for _, raw := range []string{"", " "} {
|
|
if _, ok := parseRockTime(raw); ok {
|
|
t.Fatalf("%q 不应解析成功", raw)
|
|
}
|
|
}
|
|
}
|