feat(crypto策略): 新增 AG 趋势、异动与波段跟踪信号支持
Motivation: crypto-strategy 源新增 AG 趋势(AGTS)、异动(AMA)、波段跟踪(BTS) 三类量化策略信号,需与既有 trade.* 事件区分以避免和 AI crypto signals 精确匹配冲突,并为缺省方向提供兜底推导。 Changes: * 事件名映射按 strategyCode 使用独立前缀(AGTS./AMA./BTS.,HLSS 保持不变) * direction/side 缺失时依据 isSale 兜底推导为 LONG/SHORT * 补充三类策略的模板、规则示例文档与渲染/转换测试
This commit is contained in:
@@ -102,7 +102,7 @@ make build && ./bin/server
|
|||||||
|
|
||||||
环境变量 `RABBITMQ_URL` 未设置时不启动消费,HTTP 通知不受影响。交易信号订阅需事先创建 Source(如 `trade-signal`)、模板(可用 `{{.formatted}}`)、规则 `trade.open` / `trade.add` / `trade.close` / `trade.reduce`、以及渠道。规则条件可用 `strategyCode` / `symbol` / `period`。
|
环境变量 `RABBITMQ_URL` 未设置时不启动消费,HTTP 通知不受影响。交易信号订阅需事先创建 Source(如 `trade-signal`)、模板(可用 `{{.formatted}}`)、规则 `trade.open` / `trade.add` / `trade.close` / `trade.reduce`、以及渠道。规则条件可用 `strategyCode` / `symbol` / `period`。
|
||||||
|
|
||||||
`crypto-strategy` 开仓(多/空,含原来的 `isSale` 空单)都映射为 `trade.open`,不再发 `trade.sell`。止盈(`isGain`)为 `trade.gain`,止损(`isClose` 且非 `isGain`)为 `trade.close`。高低分 `HLSS` 仍用 `HLSS.open` / `HLSS.sell` / `HLSS.close`。同一 Source 允许多条相同 event 的规则(用条件区分);精确 event 优先于通配,条件通过的规则都会发送。AI crypto signals 的现成模板与规则见 `docs/httpie/curls.md`。
|
`crypto-strategy` 开仓(多/空,含原来的 `isSale` 空单)都映射为 `trade.open`,不再发 `trade.sell`。止盈(`isGain`)为 `trade.gain`,止损(`isClose` 且非 `isGain`)为 `trade.close`。高低分 `HLSS`、异动 `AMA`、波段 `BTS`、AG 趋势 `AGTS` 用各自前缀:`HLSS.open` / `AMA.open` / `BTS.close` / `AGTS.open` 等。同一 Source 允许多条相同 event 的规则(用条件区分);精确 event 优先于通配,条件通过的规则都会发送。现成模板与规则见 `docs/httpie/curls.md`。
|
||||||
|
|
||||||
健康检查:`GET /health` → `{"status":"ok"}`
|
健康检查:`GET /health` → `{"status":"ok"}`
|
||||||
|
|
||||||
|
|||||||
@@ -565,3 +565,100 @@ curl -X POST 'http://localhost:8080/api/v1/rules' \
|
|||||||
"enabled": 1
|
"enabled": 1
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AG 趋势 / 异动 / 波段跟踪(crypto-strategy)
|
||||||
|
|
||||||
|
Source:`crypto-strategy`。Convert 后事件为 `AGTS.*` / `AMA.*` / `BTS.*`(与 `trade.*` 分开,避免和 AI crypto signals 抢精确匹配)。
|
||||||
|
|
||||||
|
| 策略 | strategyCode | 样例 payload | Convert event |
|
||||||
|
|------|--------------|--------------|---------------|
|
||||||
|
| AG趋势 | `AGTS` | `isSale` 开仓,带 `gainPrices` / `openPrice2` / `lossPrice` | `AGTS.open` |
|
||||||
|
| 异动 | `AMA` | 开仓,仅 `price` | `AMA.open` |
|
||||||
|
| 波段跟踪 | `BTS` | `isClose=true`,仅 `price` | `BTS.close` |
|
||||||
|
|
||||||
|
有效期按样例写死:AG 6 天、异动 2-4 天、波段 17h。波段周期含 `2h` → `2小时`。
|
||||||
|
|
||||||
|
### 创建 AG 趋势模板
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:8080/api/v1/templates' \
|
||||||
|
-H 'Authorization: Bearer admin-sk-change-me' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"name": "ag_trend",
|
||||||
|
"content": "监控告警提醒\n\n操作策略:AG趋势{{case .symbol \"BTCUSDT\" \"BTC\" \"ETHUSDT\" \"ETH\" \"SOLUSDT\" \"SOL\" \"BNBUSDT\" \"BNB\" .symbol}}-{{case .period \"1h\" \"1小时\" \"2h\" \"2小时\" \"4h\" \"4小时\" \"6h\" \"6小时\" \"15m\" \"15分钟\" \"5m\" \"5分钟\" \"30m\" \"30分钟\" \"1d\" \"1日\" .period}}周期{{case .side \"LONG\" \"做多\" \"SHORT\" \"做空\"}}\n\n提醒时间:{{.pushedAt}}\n\n{{with .takeProfitRange}}止盈目标:{{.}}\n\n{{else}}{{with .takeProfitPrice}}止盈目标:{{.}}\n\n{{end}}{{end}}{{with .entryRange}}介入区间:{{.}}\n\n{{else}}{{with .price}}介入区间:{{.}}\n\n{{end}}{{end}}{{with .stopLossPrice}}止损价位:{{.}}\n\n{{end}}有效期:6天"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建异动预警模板
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:8080/api/v1/templates' \
|
||||||
|
-H 'Authorization: Bearer admin-sk-change-me' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"name": "anomaly_alert",
|
||||||
|
"content": "监控告警提醒\n\n监控名称:异动预警\n\n监控时间:{{.pushedAt}}\n\n监控目标:{{case .symbol \"BTCUSDT\" \"BTC\" \"ETHUSDT\" \"ETH\" \"SOLUSDT\" \"SOL\" \"BNBUSDT\" \"BNB\" .symbol}}异动预警(暴涨/跌)生效\n\n监控提醒:异动发生概率v1(v1<v2<v3)\n\n有效期:2-4天"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建波段跟踪模板
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:8080/api/v1/templates' \
|
||||||
|
-H 'Authorization: Bearer admin-sk-change-me' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"name": "swing_track",
|
||||||
|
"content": "监控告警提醒\n\n监控名称:波段跟踪触发{{case .symbol \"BTCUSDT\" \"BTC\" \"ETHUSDT\" \"ETH\" \"SOLUSDT\" \"SOL\" \"BNBUSDT\" \"BNB\" .symbol}}-{{case .period \"1h\" \"1小时\" \"2h\" \"2小时\" \"4h\" \"4小时\" \"6h\" \"6小时\" \"15m\" \"15分钟\" \"5m\" \"5分钟\" \"30m\" \"30分钟\" \"1d\" \"1日\" .period}}周期{{case .side \"LONG\" \"做多\" \"SHORT\" \"做空\"}}\n\n监控时间:{{.pushedAt}}\n\n监控提醒:当前提醒价格{{with .takeProfitRange}}{{.}}{{else}}{{with .entryRange}}{{.}}{{else}}{{.price}}{{end}}{{end}}\n\n监控状态:等待量化信号平仓\n\n有效期: 17h"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建规则
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:8080/api/v1/rules' \
|
||||||
|
-H 'Authorization: Bearer admin-sk-change-me' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"name": "AG趋势",
|
||||||
|
"source_name": "crypto-strategy",
|
||||||
|
"event": "AGTS.*",
|
||||||
|
"template_name": "ag_trend",
|
||||||
|
"channels": ["safew_AG趋势", "safew_AG趋势02", "safew_AG趋势03"],
|
||||||
|
"conditions": [{"field": "strategyCode", "op": "eq", "value": "AGTS"}],
|
||||||
|
"enabled": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:8080/api/v1/rules' \
|
||||||
|
-H 'Authorization: Bearer admin-sk-change-me' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"name": "异动预警",
|
||||||
|
"source_name": "crypto-strategy",
|
||||||
|
"event": "AMA.*",
|
||||||
|
"template_name": "anomaly_alert",
|
||||||
|
"channels": ["safew_异动策略", "safew_异动策略02", "safew_异动策略03"],
|
||||||
|
"conditions": [{"field": "strategyCode", "op": "eq", "value": "AMA"}],
|
||||||
|
"enabled": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:8080/api/v1/rules' \
|
||||||
|
-H 'Authorization: Bearer admin-sk-change-me' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"name": "波段跟踪",
|
||||||
|
"source_name": "crypto-strategy",
|
||||||
|
"event": "BTS.*",
|
||||||
|
"template_name": "swing_track",
|
||||||
|
"channels": ["safew_波段跟踪02", "safew_波段跟踪03"],
|
||||||
|
"conditions": [{"field": "strategyCode", "op": "eq", "value": "BTS"}],
|
||||||
|
"enabled": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|||||||
@@ -62,18 +62,23 @@ func Convert(body []byte) (string, map[string]interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
action := inferAction(p)
|
action := inferAction(p)
|
||||||
event := "trade." + strings.ToLower(action)
|
event := eventName(p.StrategyCode, action)
|
||||||
if strings.EqualFold(strings.TrimSpace(p.StrategyCode), "HLSS") {
|
|
||||||
event = "HLSS." + strings.ToLower(action)
|
|
||||||
}
|
|
||||||
text := format(env, p, action)
|
text := format(env, p, action)
|
||||||
|
side := strings.ToUpper(strings.TrimSpace(env.Direction))
|
||||||
|
if side == "" {
|
||||||
|
if p.IsSale {
|
||||||
|
side = "SHORT"
|
||||||
|
} else {
|
||||||
|
side = "LONG"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"eventType": env.EventType,
|
"eventType": env.EventType,
|
||||||
"correlationId": env.CorrelationID,
|
"correlationId": env.CorrelationID,
|
||||||
"symbol": firstNonEmpty(env.Symbol, p.Currency),
|
"symbol": firstNonEmpty(env.Symbol, p.Currency),
|
||||||
"direction": env.Direction,
|
"direction": firstNonEmpty(env.Direction, side),
|
||||||
"side": strings.ToUpper(env.Direction),
|
"side": side,
|
||||||
"action": action,
|
"action": action,
|
||||||
"eventTime": env.EventTime,
|
"eventTime": env.EventTime,
|
||||||
"strategyCode": p.StrategyCode,
|
"strategyCode": p.StrategyCode,
|
||||||
@@ -171,6 +176,17 @@ func mergePayloadFields(data map[string]interface{}, payloadJSON []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func eventName(strategyCode, action string) string {
|
||||||
|
code := strings.ToUpper(strings.TrimSpace(strategyCode))
|
||||||
|
suffix := strings.ToLower(action)
|
||||||
|
switch code {
|
||||||
|
case "HLSS", "AMA", "BTS", "AGTS":
|
||||||
|
return code + "." + suffix
|
||||||
|
default:
|
||||||
|
return "trade." + suffix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func inferAction(p payload) string {
|
func inferAction(p payload) string {
|
||||||
if strings.EqualFold(strings.TrimSpace(p.StrategyCode), "HLSS") {
|
if strings.EqualFold(strings.TrimSpace(p.StrategyCode), "HLSS") {
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -433,3 +433,198 @@ func TestRenderAICryptoGainAndCloseTemplates(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const agTrendTmpl = `监控告警提醒
|
||||||
|
|
||||||
|
操作策略:AG趋势{{case .symbol "BTCUSDT" "BTC" "ETHUSDT" "ETH" "SOLUSDT" "SOL" "BNBUSDT" "BNB" .symbol}}-{{case .period "1h" "1小时" "2h" "2小时" "4h" "4小时" "6h" "6小时" "15m" "15分钟" "5m" "5分钟" "30m" "30分钟" "1d" "1日" .period}}周期{{case .side "LONG" "做多" "SHORT" "做空"}}
|
||||||
|
|
||||||
|
提醒时间:{{.pushedAt}}
|
||||||
|
|
||||||
|
{{with .takeProfitRange}}止盈目标:{{.}}
|
||||||
|
|
||||||
|
{{else}}{{with .takeProfitPrice}}止盈目标:{{.}}
|
||||||
|
|
||||||
|
{{end}}{{end}}{{with .entryRange}}介入区间:{{.}}
|
||||||
|
|
||||||
|
{{else}}{{with .price}}介入区间:{{.}}
|
||||||
|
|
||||||
|
{{end}}{{end}}{{with .stopLossPrice}}止损价位:{{.}}
|
||||||
|
|
||||||
|
{{end}}有效期:6天`
|
||||||
|
|
||||||
|
const anomalyAlertTmpl = `监控告警提醒
|
||||||
|
|
||||||
|
监控名称:异动预警
|
||||||
|
|
||||||
|
监控时间:{{.pushedAt}}
|
||||||
|
|
||||||
|
监控目标:{{case .symbol "BTCUSDT" "BTC" "ETHUSDT" "ETH" "SOLUSDT" "SOL" "BNBUSDT" "BNB" .symbol}}异动预警(暴涨/跌)生效
|
||||||
|
|
||||||
|
监控提醒:异动发生概率v1(v1<v2<v3)
|
||||||
|
|
||||||
|
有效期:2-4天`
|
||||||
|
|
||||||
|
const swingTrackTmpl = `监控告警提醒
|
||||||
|
|
||||||
|
监控名称:波段跟踪触发{{case .symbol "BTCUSDT" "BTC" "ETHUSDT" "ETH" "SOLUSDT" "SOL" "BNBUSDT" "BNB" .symbol}}-{{case .period "1h" "1小时" "2h" "2小时" "4h" "4小时" "6h" "6小时" "15m" "15分钟" "5m" "5分钟" "30m" "30分钟" "1d" "1日" .period}}周期{{case .side "LONG" "做多" "SHORT" "做空"}}
|
||||||
|
|
||||||
|
监控时间:{{.pushedAt}}
|
||||||
|
|
||||||
|
监控提醒:当前提醒价格{{with .takeProfitRange}}{{.}}{{else}}{{with .entryRange}}{{.}}{{else}}{{.price}}{{end}}{{end}}
|
||||||
|
|
||||||
|
监控状态:等待量化信号平仓
|
||||||
|
|
||||||
|
有效期: 17h`
|
||||||
|
|
||||||
|
func TestRenderAGAnomalySwingTemplates(t *testing.T) {
|
||||||
|
r := engine.NewRenderer()
|
||||||
|
|
||||||
|
ag, err := r.Render(agTrendTmpl, map[string]interface{}{
|
||||||
|
"symbol": "BTC",
|
||||||
|
"period": "6h",
|
||||||
|
"side": "LONG",
|
||||||
|
"pushedAt": "2026-08-07 16:00:20",
|
||||||
|
"takeProfitRange": "66821.8-67536.1",
|
||||||
|
"entryRange": "64938.6-64938.6",
|
||||||
|
"stopLossPrice": "62406.0",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"操作策略:AG趋势BTC-6小时周期做多",
|
||||||
|
"提醒时间:2026-08-07 16:00:20",
|
||||||
|
"止盈目标:66821.8-67536.1",
|
||||||
|
"介入区间:64938.6-64938.6",
|
||||||
|
"止损价位:62406.0",
|
||||||
|
"有效期:6天",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(ag, want) {
|
||||||
|
t.Fatalf("missing %q in\n%s", want, ag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yd, err := r.Render(anomalyAlertTmpl, map[string]interface{}{
|
||||||
|
"symbol": "BTC",
|
||||||
|
"pushedAt": "2026-07-29 00:00:50",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"监控名称:异动预警",
|
||||||
|
"监控时间:2026-07-29 00:00:50",
|
||||||
|
"监控目标:BTC异动预警(暴涨/跌)生效",
|
||||||
|
"监控提醒:异动发生概率v1(v1<v2<v3)",
|
||||||
|
"有效期:2-4天",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(yd, want) {
|
||||||
|
t.Fatalf("missing %q in\n%s", want, yd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bd, err := r.Render(swingTrackTmpl, map[string]interface{}{
|
||||||
|
"symbol": "ETH",
|
||||||
|
"period": "1h",
|
||||||
|
"side": "LONG",
|
||||||
|
"pushedAt": "2026-07-24 08:00:07",
|
||||||
|
"takeProfitRange": "1878.4-1894.2",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"监控名称:波段跟踪触发ETH-1小时周期做多",
|
||||||
|
"监控时间:2026-07-24 08:00:07",
|
||||||
|
"监控提醒:当前提醒价格1878.4-1894.2",
|
||||||
|
"监控状态:等待量化信号平仓",
|
||||||
|
"有效期: 17h",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(bd, want) {
|
||||||
|
t.Fatalf("missing %q in\n%s", want, bd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertAGTSAMAAndBTS(t *testing.T) {
|
||||||
|
agBody := []byte(`{
|
||||||
|
"eventType":"SIGNAL_RECEIVED","symbol":"BTC","direction":"SHORT",
|
||||||
|
"payload":"{\"strategyCode\":\"AGTS\",\"period\":\"6h\",\"currency\":\"BTC\",\"isSale\":true,\"isClose\":false,\"price\":63303,\"lossPrice\":65771.82,\"gainPrices\":\"61467.210000000000000,60770.880000000000000\",\"openPrice2\":63303.03,\"totalGainTarget\":2,\"leverage\":100}",
|
||||||
|
"eventTime":1786780820000
|
||||||
|
}`)
|
||||||
|
event, data, err := Convert(agBody)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if event != "AGTS.open" {
|
||||||
|
t.Fatalf("AGTS event=%q", event)
|
||||||
|
}
|
||||||
|
if data["takeProfitRange"] != "61467.21-60770.88" {
|
||||||
|
t.Fatalf("takeProfitRange=%v", data["takeProfitRange"])
|
||||||
|
}
|
||||||
|
if data["entryRange"] != "63303-63303.03" {
|
||||||
|
t.Fatalf("entryRange=%v", data["entryRange"])
|
||||||
|
}
|
||||||
|
out, err := engine.NewRenderer().Render(agTrendTmpl, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"操作策略:AG趋势BTC-6小时周期做空",
|
||||||
|
"止盈目标:61467.21-60770.88",
|
||||||
|
"介入区间:63303-63303.03",
|
||||||
|
"止损价位:65771.82",
|
||||||
|
"有效期:6天",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(out, want) {
|
||||||
|
t.Fatalf("missing %q in\n%s", want, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
amaBody := []byte(`{
|
||||||
|
"eventType":"SIGNAL_RECEIVED","symbol":"BTC","direction":"LONG",
|
||||||
|
"payload":"{\"strategyCode\":\"AMA\",\"period\":\"4h\",\"currency\":\"BTC\",\"isSale\":false,\"isClose\":false,\"price\":63119.9,\"leverage\":100}",
|
||||||
|
"eventTime":1785312050000
|
||||||
|
}`)
|
||||||
|
event, data, err = Convert(amaBody)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if event != "AMA.open" || data["strategyCode"] != "AMA" {
|
||||||
|
t.Fatalf("AMA event=%q data=%v", event, data)
|
||||||
|
}
|
||||||
|
out, err = engine.NewRenderer().Render(anomalyAlertTmpl, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "监控目标:BTC异动预警(暴涨/跌)生效") {
|
||||||
|
t.Fatalf("%s", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
btsBody := []byte(`{
|
||||||
|
"eventType":"SIGNAL_RECEIVED","symbol":"BTC","direction":"LONG",
|
||||||
|
"payload":"{\"strategyCode\":\"BTS\",\"period\":\"2h\",\"currency\":\"BTC\",\"isSale\":false,\"isClose\":true,\"isGain\":false,\"price\":63533.2}",
|
||||||
|
"eventTime":1784865607000
|
||||||
|
}`)
|
||||||
|
event, data, err = Convert(btsBody)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if event != "BTS.close" {
|
||||||
|
t.Fatalf("BTS event=%q want BTS.close", event)
|
||||||
|
}
|
||||||
|
out, err = engine.NewRenderer().Render(swingTrackTmpl, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"监控名称:波段跟踪触发BTC-2小时周期做多",
|
||||||
|
"监控提醒:当前提醒价格63533.2",
|
||||||
|
"监控状态:等待量化信号平仓",
|
||||||
|
"有效期: 17h",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(out, want) {
|
||||||
|
t.Fatalf("missing %q in\n%s", want, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user