fix: map decrypted CoinGlass liq heatmap payload
Some checks failed
ci / build + vet + guard + race (push) Has been cancelled
Some checks failed
ci / build + vet + guard + race (push) Has been cancelled
This commit is contained in:
@@ -11,46 +11,65 @@ import (
|
|||||||
"cryptoHermes/internal/entity"
|
"cryptoHermes/internal/entity"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CGEnvelope 是所有 CoinGlass V2 endpoint 的统一外壳。
|
// CGEnvelope 是 CoinGlass V2 endpoint 的兼容外壳。
|
||||||
// 解密后的 plaintext JSON 形如:{"success":true,"code":"0","msg":"","data":{...}}
|
// HTTP body 解密前有 success/code/data;解密后的 plaintext 通常已经是 data 本体。
|
||||||
|
// 测试和旧 dump 可能仍喂 envelope,mapper 保留兼容。
|
||||||
type CGEnvelope struct {
|
type CGEnvelope struct {
|
||||||
Success bool `json:"success"`
|
Success *bool `json:"success"`
|
||||||
Code json.Number `json:"code"`
|
Code json.RawMessage `json:"code"`
|
||||||
Msg string `json:"msg"`
|
Msg string `json:"msg"`
|
||||||
Data json.RawMessage `json:"data"`
|
Data json.RawMessage `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MapLiqHeatMap 把解密后的 plaintext envelope 映射成 entity.CGLiqHeatMap。
|
// MapLiqHeatMap 把解密后的 plaintext 映射成 entity.CGLiqHeatMap。
|
||||||
//
|
//
|
||||||
// PR-1 阶段未拿到真实 schema dump,先做最薄映射:data 整体进 RawGrid;
|
// 真实响应的 plaintext 是 data 本体(例如 {"instrument":...,"liq":[...]})。
|
||||||
|
// 为兼容早期 fixture,如 plaintext 仍带 success/code/data envelope,则先拆 data。
|
||||||
|
// PR-1 阶段做最薄映射:data 整体进 RawGrid;
|
||||||
// 若顶层有 minPrice/maxPrice/priceMin/priceMax 字段则抽出(容忍命名漂移)。
|
// 若顶层有 minPrice/maxPrice/priceMin/priceMax 字段则抽出(容忍命名漂移)。
|
||||||
// 真实 schema 经 spike dump 后回填到 spec §5(mapper 同步加显式字段)。
|
// 真实 schema 经 spike dump 后回填到 spec §5(mapper 同步加显式字段)。
|
||||||
//
|
//
|
||||||
// 数值边界(ADR-0004 D3):解 JSON number 默认 float64 是 transient,
|
// 数值边界(ADR-0004 D3):解 JSON number 默认 float64 是 transient,
|
||||||
// 通过 formatFloat 转 string 进 entity;entity 字段全 string(G12.4 兜底)。
|
// 通过 formatFloat 转 string 进 entity;entity 字段全 string(G12.4 兜底)。
|
||||||
func MapLiqHeatMap(symbol string, interval string, ts int64, plaintext []byte) (*entity.CGLiqHeatMap, error) {
|
func MapLiqHeatMap(symbol string, interval string, ts int64, plaintext []byte) (*entity.CGLiqHeatMap, error) {
|
||||||
var env CGEnvelope
|
raw, err := liqHeatMapDataPayload(plaintext)
|
||||||
if err := json.Unmarshal(plaintext, &env); err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("liq_heatmap envelope: %w", err)
|
return nil, err
|
||||||
}
|
|
||||||
if !env.Success {
|
|
||||||
return nil, fmt.Errorf("liq_heatmap business error code=%s msg=%s", env.Code, env.Msg)
|
|
||||||
}
|
}
|
||||||
out := &entity.CGLiqHeatMap{
|
out := &entity.CGLiqHeatMap{
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
Interval: interval,
|
Interval: interval,
|
||||||
Timestamp: ts,
|
Timestamp: ts,
|
||||||
RawGrid: append(json.RawMessage(nil), env.Data...),
|
RawGrid: append(json.RawMessage(nil), raw...),
|
||||||
}
|
}
|
||||||
|
|
||||||
var probe map[string]json.RawMessage
|
var probe map[string]json.RawMessage
|
||||||
if err := json.Unmarshal(env.Data, &probe); err == nil {
|
if err := json.Unmarshal(raw, &probe); err == nil {
|
||||||
out.PriceMin = pickFloatField(probe, "priceMin", "minPrice", "min")
|
out.PriceMin = pickFloatField(probe, "priceMin", "minPrice", "min")
|
||||||
out.PriceMax = pickFloatField(probe, "priceMax", "maxPrice", "max")
|
out.PriceMax = pickFloatField(probe, "priceMax", "maxPrice", "max")
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func liqHeatMapDataPayload(plaintext []byte) (json.RawMessage, error) {
|
||||||
|
if !json.Valid(plaintext) {
|
||||||
|
return nil, fmt.Errorf("liq_heatmap plaintext: invalid json")
|
||||||
|
}
|
||||||
|
|
||||||
|
var env CGEnvelope
|
||||||
|
if err := json.Unmarshal(plaintext, &env); err == nil && env.Success != nil {
|
||||||
|
if !*env.Success {
|
||||||
|
return nil, fmt.Errorf("%w: code=%s msg=%q", ErrAPIBusiness, envelopeCode(env.Code), env.Msg)
|
||||||
|
}
|
||||||
|
if len(env.Data) == 0 || string(env.Data) == "null" {
|
||||||
|
return nil, fmt.Errorf("%w: empty data code=%s msg=%q", ErrFakeSuccess, envelopeCode(env.Code), env.Msg)
|
||||||
|
}
|
||||||
|
return append(json.RawMessage(nil), env.Data...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(json.RawMessage(nil), plaintext...), nil
|
||||||
|
}
|
||||||
|
|
||||||
// pickFloatField 在 probe map 里按候选 key 顺序找第一个能解成 float64 的字段,
|
// pickFloatField 在 probe map 里按候选 key 顺序找第一个能解成 float64 的字段,
|
||||||
// 用 formatFloat 转回 string。找不到返回 ""。
|
// 用 formatFloat 转回 string。找不到返回 ""。
|
||||||
func pickFloatField(probe map[string]json.RawMessage, keys ...string) string {
|
func pickFloatField(probe map[string]json.RawMessage, keys ...string) string {
|
||||||
|
|||||||
@@ -11,16 +11,24 @@ import (
|
|||||||
func TestMapLiqHeatMap_FloatToString(t *testing.T) {
|
func TestMapLiqHeatMap_FloatToString(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
envelope string
|
plaintext string
|
||||||
wantSymbol string
|
wantSymbol string
|
||||||
wantMin string
|
wantMin string
|
||||||
wantMax string
|
wantMax string
|
||||||
wantErr bool
|
wantErr bool
|
||||||
probeRawKey string
|
probeRawKey string
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
name: "real decrypted data object",
|
||||||
|
plaintext: `{"instrument":{"exName":"Binance","instrumentId":"BTCUSDT"},"liq":[[1,2,3.4]],"priceMin":110234.5,"priceMax":118888.0}`,
|
||||||
|
wantSymbol: "Binance_BTCUSDT",
|
||||||
|
wantMin: "110234.5",
|
||||||
|
wantMax: "118888",
|
||||||
|
probeRawKey: "liq",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "happy path with float number fields",
|
name: "happy path with float number fields",
|
||||||
envelope: `{"success":true,"code":"0","msg":"","data":{"priceMin":110234.5,"priceMax":118888.0,"grid":[[1,2],[3,4]]}}`,
|
plaintext: `{"success":true,"code":"0","msg":"","data":{"priceMin":110234.5,"priceMax":118888.0,"grid":[[1,2],[3,4]]}}`,
|
||||||
wantSymbol: "Binance_BTCUSDT",
|
wantSymbol: "Binance_BTCUSDT",
|
||||||
wantMin: "110234.5",
|
wantMin: "110234.5",
|
||||||
wantMax: "118888",
|
wantMax: "118888",
|
||||||
@@ -28,35 +36,35 @@ func TestMapLiqHeatMap_FloatToString(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "string-typed numbers are passed through unchanged",
|
name: "string-typed numbers are passed through unchanged",
|
||||||
envelope: `{"success":true,"code":"0","msg":"","data":{"priceMin":"99999.99","priceMax":"100000.01"}}`,
|
plaintext: `{"success":true,"code":"0","msg":"","data":{"priceMin":"99999.99","priceMax":"100000.01"}}`,
|
||||||
wantSymbol: "Binance_BTCUSDT",
|
wantSymbol: "Binance_BTCUSDT",
|
||||||
wantMin: "99999.99",
|
wantMin: "99999.99",
|
||||||
wantMax: "100000.01",
|
wantMax: "100000.01",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "alternate field names (minPrice/maxPrice)",
|
name: "alternate field names (minPrice/maxPrice)",
|
||||||
envelope: `{"success":true,"code":"0","msg":"","data":{"minPrice":1000.5,"maxPrice":2000.5}}`,
|
plaintext: `{"success":true,"code":"0","msg":"","data":{"minPrice":1000.5,"maxPrice":2000.5}}`,
|
||||||
wantSymbol: "Binance_BTCUSDT",
|
wantSymbol: "Binance_BTCUSDT",
|
||||||
wantMin: "1000.5",
|
wantMin: "1000.5",
|
||||||
wantMax: "2000.5",
|
wantMax: "2000.5",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing min/max fields → empty strings",
|
name: "missing min/max fields → empty strings",
|
||||||
envelope: `{"success":true,"code":"0","msg":"","data":{"grid":[]}}`,
|
plaintext: `{"success":true,"code":"0","msg":"","data":{"grid":[]}}`,
|
||||||
wantSymbol: "Binance_BTCUSDT",
|
wantSymbol: "Binance_BTCUSDT",
|
||||||
wantMin: "",
|
wantMin: "",
|
||||||
wantMax: "",
|
wantMax: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "business error propagates",
|
name: "business error propagates",
|
||||||
envelope: `{"success":false,"code":"40001","msg":"bad params","data":null}`,
|
plaintext: `{"success":false,"code":"40001","msg":"bad params","data":null}`,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
got, err := MapLiqHeatMap("Binance_BTCUSDT", "5", 1735689600000, []byte(tc.envelope))
|
got, err := MapLiqHeatMap("Binance_BTCUSDT", "5", 1735689600000, []byte(tc.plaintext))
|
||||||
if tc.wantErr {
|
if tc.wantErr {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ func (c *CoinglassCollector) CollectLiqHeatMap(ctx context.Context) error {
|
|||||||
|
|
||||||
item, err := c.d.Provider.GetLiqHeatMap(ctx, symbol, c.d.LiqHeatMapInterval, c.d.LiqHeatMapLimit)
|
item, err := c.d.Provider.GetLiqHeatMap(ctx, symbol, c.d.LiqHeatMapInterval, c.d.LiqHeatMapLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, cgapi.ErrAPIBusiness) {
|
||||||
|
c.d.Logger.Warn("coinglass_liq_heatmap_business_error", "symbol", symbol, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
hadError = true
|
hadError = true
|
||||||
c.recordLiqHeatMapError(symbol, err)
|
c.recordLiqHeatMapError(symbol, err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user