feat(通知模板): 支持缺字段按空值渲染与空行省略

Motivation:
统一 crypto-strategy 与 trade-signal 两类交易信号的模板字段,使同一套通知模板可复用;字段缺失或为空时不再导致渲染报错或输出空行,通知内容更整洁。

Changes:

* 渲染器缺失字段改为按空值处理,新增 line 函数实现空值整行省略
* crypto-strategy 信号补充止盈价、止损价、平均价、收益额等模板字段
* trade-signal 信号保留原始报文中的额外字段以适配统一模板
* 修复结构化日志将 error 作为值直接输出导致的格式问题
This commit is contained in:
2026-08-16 00:19:50 +08:00
parent de7a52e81f
commit ae25409e56
9 changed files with 197 additions and 43 deletions
+36 -1
View File
@@ -3,6 +3,7 @@ package engine
import (
"bytes"
"fmt"
"strings"
"text/template"
)
@@ -13,7 +14,10 @@ func NewRenderer() *Renderer {
}
func (r *Renderer) Render(tmplContent string, data map[string]interface{}) (string, error) {
tmpl, err := template.New("notify").Option("missingkey=error").Parse(tmplContent)
tmpl, err := template.New("notify").
Option("missingkey=zero").
Funcs(template.FuncMap{"line": templateLine}).
Parse(tmplContent)
if err != nil {
return "", fmt.Errorf("parse template: %w", err)
}
@@ -23,3 +27,34 @@ func (r *Renderer) Render(tmplContent string, data map[string]interface{}) (stri
}
return buf.String(), nil
}
// templateLine renders "labelvalue\n" or empty if value is missing/zero.
func templateLine(label string, v any) string {
if isEmptyValue(v) {
return ""
}
return label + "" + fmt.Sprint(v) + "\n"
}
func isEmptyValue(v any) bool {
switch t := v.(type) {
case nil:
return true
case string:
return strings.TrimSpace(t) == ""
case bool:
return !t
case int:
return t == 0
case int32:
return t == 0
case int64:
return t == 0
case float32:
return t == 0
case float64:
return t == 0
default:
return false
}
}
+35 -4
View File
@@ -18,10 +18,41 @@ func TestRenderer(t *testing.T) {
}
}
func TestRenderer_Error(t *testing.T) {
func TestRendererMissingKeyEmpty(t *testing.T) {
r := NewRenderer()
_, err := r.Render("{{.nonexistent}}", map[string]interface{}{})
if err == nil {
t.Error("expected error for missing field, got nil")
_, err := r.Render("x{{.nonexistent}}y", map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
}
func TestRendererLineOmitsEmpty(t *testing.T) {
r := NewRenderer()
tmpl := `{{line "币种" .symbol}}{{line "平均价" .totalAvgPx}}{{line "价格" .price}}{{line "止损价" .stopLossPrice}}`
out, err := r.Render(tmpl, map[string]interface{}{
"symbol": "ICP",
"price": 2.273,
"totalAvgPx": "",
"stopLossPrice": 0,
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "币种:ICP") || !strings.Contains(out, "价格:2.273") {
t.Fatalf("out=%q", out)
}
if strings.Contains(out, "平均价") || strings.Contains(out, "止损价") {
t.Fatalf("empty lines should be omitted, out=%q", out)
}
}
func TestRendererLineMissingKey(t *testing.T) {
r := NewRenderer()
out, err := r.Render(`{{line "平均价" .totalAvgPx}}{{line "币种" .symbol}}`, map[string]interface{}{"symbol": "ICP"})
if err != nil {
t.Fatal(err)
}
if strings.Contains(out, "平均价") || !strings.Contains(out, "币种:ICP") {
t.Fatalf("out=%q", out)
}
}