From 14504af950c8e373b0849620a782664f72f31768 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 16 Aug 2026 01:18:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=80=9A=E7=9F=A5):=20SafeW=20=E5=8F=AA?= =?UTF-8?q?=E5=8F=91=E6=AD=A3=E6=96=87=E5=B9=B6=E6=B3=A8=E5=85=A5=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 去掉渠道标题前缀,模板可用 pushedAt 显示本地推送时间。 --- README.md | 6 ++++-- internal/adapter/safew.go | 2 +- internal/adapter/safew_test.go | 2 +- internal/notify/service.go | 4 ++++ internal/notify/service_test.go | 34 +++++++++++++++++++++++++++++++-- 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 77ba04f..bf7424f 100644 --- a/README.md +++ b/README.md @@ -307,9 +307,11 @@ Body 同创建。成功:`{"ok": true}` ``` ### {{.symbol}} {{case .action "OPEN" "开仓" "CLOSE" "平仓" "GAIN" "止盈" "SELL" "卖出" "ADD" "加仓" "REDUCE" "减仓"}} -{{line "币种" .symbol}}{{line "周期" .period}}{{line "方向" .side}}{{line "价格" .price}}{{line "平均价" .totalAvgPx}}{{line "止盈价" .takeProfitPrice}}{{line "止损价" .stopLossPrice}} +{{line "币种" .symbol}}{{line "周期" .period}}{{line "方向" .side}}{{line "价格" .price}}{{line "平均价" .totalAvgPx}}{{line "止盈价" .takeProfitPrice}}{{line "止损价" .stopLossPrice}}{{line "推送时间" .pushedAt}} ``` +渲染时会自动注入 `event`、`pushedAt`(本地时间 `2006.01.02 15:04:05`)。 + 也可用事件名:`{{case .event ".open" "开仓" ".close" "平仓"}}` 等价写法:`{{with .totalAvgPx}}平均价:{{.}}{{end}}` @@ -426,7 +428,7 @@ SMTP 使用全局 `config.yaml` 的 `smtp` 段;支持 587 STARTTLS / 465 TLS } ``` -`chat_id` 可为数字或字符串(含 `@username`)。消息以纯文本发送,不做 Markdown 转义。 +`chat_id` 可为数字或字符串(含 `@username`)。消息以纯文本发送正文,不附带 `{source}: {event}` 标题,也不做 Markdown 转义。 #### `POST /api/v1/channels/safew/chats` — 列出已监控的 SafeW 群 diff --git a/internal/adapter/safew.go b/internal/adapter/safew.go index d7b504f..e44a55e 100644 --- a/internal/adapter/safew.go +++ b/internal/adapter/safew.go @@ -53,7 +53,7 @@ func (s *SafeWSender) Send(title, content string, config json.RawMessage) error payload := safewMessage{ ChatID: chatID, - Text: title + "\n" + content, + Text: content, } body, err := json.Marshal(payload) if err != nil { diff --git a/internal/adapter/safew_test.go b/internal/adapter/safew_test.go index b3059b4..cae7f41 100644 --- a/internal/adapter/safew_test.go +++ b/internal/adapter/safew_test.go @@ -49,7 +49,7 @@ func TestSafeWSenderSendSuccess(t *testing.T) { if gotBody["parse_mode"] != nil && gotBody["parse_mode"] != "" { t.Fatalf("parse_mode = %#v, want omitted/plain", gotBody["parse_mode"]) } - wantText := "hello_world.\nprice=1.5" + wantText := "price=1.5" if gotBody["text"] != wantText { t.Fatalf("text = %#v, want %#v", gotBody["text"], wantText) } diff --git a/internal/notify/service.go b/internal/notify/service.go index 2cf8fce..ccb7d7c 100644 --- a/internal/notify/service.go +++ b/internal/notify/service.go @@ -8,6 +8,7 @@ import ( "log/slog" "strconv" "strings" + "time" "aiaa-notification-service/internal/condition" "aiaa-notification-service/internal/engine" @@ -85,6 +86,9 @@ func (s *Service) Process(ctx context.Context, req Request) (Result, error) { if _, ok := req.Data["event"]; !ok && req.Event != "" { 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") + } content, err := s.renderer.Render(tmpl.Content, req.Data) if err != nil { diff --git a/internal/notify/service_test.go b/internal/notify/service_test.go index 7a1c5db..a185111 100644 --- a/internal/notify/service_test.go +++ b/internal/notify/service_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "strings" "testing" "aiaa-notification-service/internal/engine" @@ -28,9 +29,15 @@ func (f *fakeTemplates) GetTemplate(context.Context, int) (*model.Template, erro return f.tmpl, f.err } -type fakeRouter struct{ channels []string } +type fakeRouter struct { + channels []string + title string + content string +} -func (f *fakeRouter) Route(context.Context, *model.Rule, string, string) []string { +func (f *fakeRouter) Route(_ context.Context, _ *model.Rule, title, content string) []string { + f.title = title + f.content = content return f.channels } @@ -111,6 +118,29 @@ func TestProcessTemplateCanSwitchOnEvent(t *testing.T) { } } +func TestProcessInjectsPushedAt(t *testing.T) { + rt := &fakeRouter{channels: []string{"safew:1"}} + svc := newSvc( + &fakeMatcher{rule: &model.Rule{ID: 9, TemplateID: 1}}, + &fakeTemplates{tmpl: &model.Template{ID: 1, Content: `{{line "推送时间" .pushedAt}}`}}, + rt, + ) + res, err := svc.Process(context.Background(), Request{ + Source: &model.Source{ID: 1, Name: "crypto-strategy"}, + Event: "trade.open", + Data: map[string]interface{}{"symbol": "QNT"}, + }) + if err != nil { + t.Fatal(err) + } + if !res.Matched { + t.Fatalf("%+v", res) + } + if !strings.Contains(rt.content, "推送时间:") { + t.Fatalf("content=%q", rt.content) + } +} + func TestProcessInvalidConditions(t *testing.T) { raw := json.RawMessage(`not-json`) svc := newSvc(&fakeMatcher{rule: &model.Rule{ID: 1, TemplateID: 1, Conditions: &raw}}, &fakeTemplates{}, &fakeRouter{})