feat(通知): SafeW 只发正文并注入推送时间

去掉渠道标题前缀,模板可用 pushedAt 显示本地推送时间。
This commit is contained in:
2026-08-16 01:18:27 +08:00
parent a9b6234208
commit 14504af950
5 changed files with 42 additions and 6 deletions
+4 -2
View File
@@ -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 群
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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)
}
+4
View File
@@ -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 {
+32 -2
View File
@@ -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{})