From a93078dc00442b7bba66d263292d4462de600207 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 17 Aug 2026 01:29:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=97=B6=E5=8C=BA):=20=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E7=BB=9F=E4=B8=80=E4=BD=BF=E7=94=A8=20UTC+8?= =?UTF-8?q?=20=E8=80=8C=E9=9D=9E=E6=9C=8D=E5=8A=A1=E5=99=A8=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E6=97=B6=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: 推送时间原先依赖 time.Local,部署在不同时区的服务器会产生不一致的展示结果。统一为 UTC+8,确保所有订阅者看到的时间固定为北京时间。 Changes: * 新增 internal/tz 时区工具,集中定义 UTC+8 与格式化逻辑 * 通知、crypto 策略、交易信号的时间渲染统一改用 UTC+8 * 补充测试断言 pushedAt 为 UTC+8 当前时间 * 更新 README 中关于注入时间的说明 Breaking Changes: 所有推送时间字段将按 UTC+8 展示,若服务部署在非 UTC+8 时区且依赖原本地时间行为,输出会发生变化。 --- README.md | 2 +- internal/notify/service.go | 3 ++- internal/notify/service_test.go | 11 +++++++++++ internal/subscriber/cryptostrategy/convert.go | 6 ++++-- .../subscriber/cryptostrategy/convert_test.go | 19 +++++++++++++++++++ internal/subscriber/tradesignal/format.go | 5 +++-- internal/tz/tz.go | 9 +++++++++ 7 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 internal/tz/tz.go diff --git a/README.md b/README.md index 4953ab0..04cd322 100644 --- a/README.md +++ b/README.md @@ -312,7 +312,7 @@ Body 同创建。成功:`{"ok": true}` {{line "币种" .symbol}}{{line "周期" .period}}{{line "方向" .side}}{{line "价格" .price}}{{line "平均价" .totalAvgPx}}{{line "止盈价" .takeProfitPrice}}{{line "止损价" .stopLossPrice}}{{line "推送时间" .pushedAt}} ``` -渲染时会自动注入 `event`、`pushedAt`(本地时间 `2006.01.02 15:04:05`)。 +渲染时会自动注入 `event`、`pushedAt`(UTC+8,`2006.01.02 15:04:05`)。 也可用事件名:`{{case .event ".open" "开仓" ".close" "平仓"}}` diff --git a/internal/notify/service.go b/internal/notify/service.go index c1139e4..6235390 100644 --- a/internal/notify/service.go +++ b/internal/notify/service.go @@ -13,6 +13,7 @@ import ( "aiaa-notification-service/internal/condition" "aiaa-notification-service/internal/engine" "aiaa-notification-service/internal/model" + "aiaa-notification-service/internal/tz" ) var ErrUnprocessable = errors.New("unprocessable") @@ -71,7 +72,7 @@ func (s *Service) Process(ctx context.Context, req Request) (Result, error) { req.Data["event"] = req.Event } if _, ok := req.Data["pushedAt"]; !ok { - req.Data["pushedAt"] = time.Now().In(time.Local).Format("2006.01.02 15:04:05") + req.Data["pushedAt"] = tz.Format(time.Now(), "2006.01.02 15:04:05") } var channels []string diff --git a/internal/notify/service_test.go b/internal/notify/service_test.go index 9e42d5f..5a8c268 100644 --- a/internal/notify/service_test.go +++ b/internal/notify/service_test.go @@ -6,6 +6,7 @@ import ( "errors" "strings" "testing" + "time" "aiaa-notification-service/internal/engine" "aiaa-notification-service/internal/model" @@ -153,6 +154,16 @@ func TestProcessInjectsPushedAt(t *testing.T) { if !strings.Contains(rt.content, "推送时间:") { t.Fatalf("content=%q", rt.content) } + got := strings.TrimPrefix(rt.content, "推送时间:") + got = strings.TrimSpace(got) + cst := time.FixedZone("CST", 8*3600) + parsed, err := time.ParseInLocation("2006.01.02 15:04:05", got, cst) + if err != nil { + t.Fatalf("parse %q: %v", got, err) + } + if d := time.Since(parsed); d < -2*time.Second || d > 2*time.Second { + t.Fatalf("pushedAt %q is not UTC+8 now, drift=%s", got, d) + } } func TestProcessInvalidConditions(t *testing.T) { diff --git a/internal/subscriber/cryptostrategy/convert.go b/internal/subscriber/cryptostrategy/convert.go index 01ff3e2..5f46280 100644 --- a/internal/subscriber/cryptostrategy/convert.go +++ b/internal/subscriber/cryptostrategy/convert.go @@ -7,6 +7,8 @@ import ( "strconv" "strings" "time" + + "aiaa-notification-service/internal/tz" ) type envelope struct { @@ -95,7 +97,7 @@ func Convert(body []byte) (string, map[string]interface{}, error) { } mergePayloadFields(data, payloadJSON) if env.EventTime > 0 { - data["pushedAt"] = time.UnixMilli(env.EventTime).In(time.Local).Format("2006-01-02 15:04:05") + data["pushedAt"] = tz.Format(time.UnixMilli(env.EventTime), "2006-01-02 15:04:05") } if p.TotalGainTarget != 0 { data["totalGainTarget"] = p.TotalGainTarget @@ -260,7 +262,7 @@ func format(env envelope, p payload, action string) string { lines = append(lines, fmt.Sprintf("策略: %s", p.StrategyCode)) } if env.EventTime > 0 { - t := time.UnixMilli(env.EventTime).In(time.Local) + t := time.UnixMilli(env.EventTime).In(tz.CST) lines = append(lines, fmt.Sprintf("Time: %s", t.Format("2006.01.02 15:04:05"))) } return strings.Join(lines, "\n") diff --git a/internal/subscriber/cryptostrategy/convert_test.go b/internal/subscriber/cryptostrategy/convert_test.go index d88ae95..357845c 100644 --- a/internal/subscriber/cryptostrategy/convert_test.go +++ b/internal/subscriber/cryptostrategy/convert_test.go @@ -50,6 +50,25 @@ func TestConvertParsesNestedPayload(t *testing.T) { } } +func TestConvertPushedAtIsUTC8(t *testing.T) { + body := []byte(`{ + "eventType":"SIGNAL_RECEIVED","symbol":"CRV","direction":"LONG", + "payload":"{\"strategyCode\":\"ai-crypto-signals\",\"isClose\":true,\"price\":0.2528}", + "eventTime":1786899539730 + }`) + _, data, err := Convert(body) + if err != nil { + t.Fatal(err) + } + if data["pushedAt"] != "2026-08-17 00:58:59" { + t.Fatalf("pushedAt=%v want UTC+8 2026-08-17 00:58:59", data["pushedAt"]) + } + formatted, _ := data["formatted"].(string) + if !strings.Contains(formatted, "Time: 2026.08.17 00:58:59") { + t.Fatalf("formatted=%s", formatted) + } +} + func TestConvertCloseFlag(t *testing.T) { body := []byte(`{ "eventType":"SIGNAL_RECEIVED","symbol":"BTC","direction":"SHORT", diff --git a/internal/subscriber/tradesignal/format.go b/internal/subscriber/tradesignal/format.go index de571a7..b374fd2 100644 --- a/internal/subscriber/tradesignal/format.go +++ b/internal/subscriber/tradesignal/format.go @@ -3,7 +3,8 @@ package tradesignal import ( "fmt" "strings" - "time" + + "aiaa-notification-service/internal/tz" ) type FormatOptions struct { @@ -85,7 +86,7 @@ func Format(signal *Signal, opts ...FormatOptions) string { lines = append(lines, fmt.Sprintf("策略: %s", signal.StrategyCode)) } - eventTime := signal.ParsedEventTime().In(time.Local) + eventTime := signal.ParsedEventTime().In(tz.CST) lines = append(lines, fmt.Sprintf("Time: %s", eventTime.Format("2006.01.02 15:04:05"))) return strings.Join(lines, "\n") diff --git a/internal/tz/tz.go b/internal/tz/tz.go new file mode 100644 index 0000000..2117e63 --- /dev/null +++ b/internal/tz/tz.go @@ -0,0 +1,9 @@ +package tz + +import "time" + +var CST = time.FixedZone("CST", 8*3600) + +func Format(t time.Time, layout string) string { + return t.In(CST).Format(layout) +}