6f5aaa9f85
Motivation: 此前的修复仅覆盖直接拼接文案的场景,跟单等基于模板渲染的推送场景中,%.2f 格式化仍会把 PEPE 等 Meme 币的极小价格(如 0.00000059)折叠为 0.00,导致交易通知丢失真实价格、误导用户。 Changes: * 抽取共享的价格显示逻辑到统一显示层,两个订阅者改为复用,移除重复实现 * 新增 Decimal 类型,渲染时以副本方式包装模板数据,使模板内 printf 风格格式化保留极小价格精度且不出现科学计数法,同时不修改调用方原始数据 * 渲染引擎空值判断改用反射实现,覆盖全部整型、无符号整型与浮点类型 * 补充转换到渲染的端到端回归测试,覆盖极小价格精度、科学计数法与数据不可变性
102 lines
2.2 KiB
Go
102 lines
2.2 KiB
Go
package engine
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"reflect"
|
||
"strings"
|
||
"text/template"
|
||
|
||
"aiaa-notification-service/internal/display"
|
||
)
|
||
|
||
type Renderer struct{}
|
||
|
||
func NewRenderer() *Renderer {
|
||
return &Renderer{}
|
||
}
|
||
|
||
func (r *Renderer) Render(tmplContent string, data map[string]interface{}) (string, error) {
|
||
tmpl, err := template.New("notify").
|
||
Option("missingkey=zero").
|
||
Funcs(template.FuncMap{
|
||
"line": templateLine,
|
||
"case": templateCase,
|
||
"replace": strings.ReplaceAll,
|
||
}).
|
||
Parse(tmplContent)
|
||
if err != nil {
|
||
return "", fmt.Errorf("parse template: %w", err)
|
||
}
|
||
var buf bytes.Buffer
|
||
if err := tmpl.Execute(&buf, display.WrapMap(data)); err != nil {
|
||
return "", fmt.Errorf("execute template: %w", err)
|
||
}
|
||
return buf.String(), nil
|
||
}
|
||
|
||
// templateLine renders "label:value\n" or empty if value is missing/zero.
|
||
func templateLine(label string, v any) string {
|
||
if isEmptyValue(v) {
|
||
return ""
|
||
}
|
||
return strings.TrimRight(label, "::") + ":" + fmt.Sprint(v) + "\n"
|
||
}
|
||
|
||
func templateCase(value any, pairs ...any) string {
|
||
got := stringify(value)
|
||
n := len(pairs)
|
||
def := got
|
||
if n%2 == 1 {
|
||
def = stringify(pairs[n-1])
|
||
pairs = pairs[:n-1]
|
||
}
|
||
for i := 0; i+1 < len(pairs); i += 2 {
|
||
if caseKeyMatch(got, stringify(pairs[i])) {
|
||
return stringify(pairs[i+1])
|
||
}
|
||
}
|
||
return def
|
||
}
|
||
|
||
func caseKeyMatch(value, key string) bool {
|
||
v := strings.ToLower(strings.TrimSpace(value))
|
||
k := strings.ToLower(strings.TrimSpace(key))
|
||
k = strings.TrimPrefix(k, ".")
|
||
vSeg := v
|
||
if i := strings.LastIndex(v, "."); i >= 0 {
|
||
vSeg = v[i+1:]
|
||
}
|
||
return k == v || k == strings.TrimPrefix(v, ".") || k == vSeg
|
||
}
|
||
|
||
func stringify(v any) string {
|
||
if v == nil {
|
||
return ""
|
||
}
|
||
return fmt.Sprint(v)
|
||
}
|
||
|
||
func isEmptyValue(v any) bool {
|
||
switch t := v.(type) {
|
||
case nil:
|
||
return true
|
||
case string:
|
||
return strings.TrimSpace(t) == ""
|
||
case bool:
|
||
return !t
|
||
default:
|
||
rv := reflect.ValueOf(v)
|
||
switch rv.Kind() {
|
||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||
return rv.Int() == 0
|
||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||
return rv.Uint() == 0
|
||
case reflect.Float32, reflect.Float64:
|
||
return rv.Float() == 0
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
}
|