diff --git a/vehicle-data-platform/apps/api/internal/platform/handler.go b/vehicle-data-platform/apps/api/internal/platform/handler.go index 2d17c41f..8a616f06 100644 --- a/vehicle-data-platform/apps/api/internal/platform/handler.go +++ b/vehicle-data-platform/apps/api/internal/platform/handler.go @@ -186,6 +186,10 @@ func (h *Handler) handleOpsHealth(w http.ResponseWriter, r *http.Request) { func (h *Handler) write(w http.ResponseWriter, r *http.Request, data any, err error) { if err != nil { + if clientErr, ok := asClientError(err); ok { + httpx.WriteError(w, http.StatusBadRequest, clientErr.Code, clientErr.Message, "", traceID(r)) + return + } httpx.WriteError(w, http.StatusInternalServerError, "INTERNAL", "服务处理失败", err.Error(), traceID(r)) return } diff --git a/vehicle-data-platform/apps/api/internal/platform/handler_test.go b/vehicle-data-platform/apps/api/internal/platform/handler_test.go index 125b9fe8..e9df9b0d 100644 --- a/vehicle-data-platform/apps/api/internal/platform/handler_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/handler_test.go @@ -941,6 +941,47 @@ func TestHandlerRawFramesPostAcceptsKeyword(t *testing.T) { } } +func TestHandlerRawFramesRejectsUnscopedIncludeFields(t *testing.T) { + handler := NewHandler(NewService(NewMockStore())) + cases := []struct { + name string + req *http.Request + }{ + { + name: "get", + req: httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?limit=1&includeFields=true", nil), + }, + { + name: "post", + req: httptest.NewRequest(http.MethodPost, "/api/history/raw-frames/query", strings.NewReader(`{"limit":1,"includeFields":true}`)), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, tc.req) + if rec.Code != http.StatusBadRequest { + t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String()) + } + var body struct { + Error struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("response JSON should decode: %v body=%s", err, rec.Body.String()) + } + if body.Error.Code != "RAW_FRAME_SCOPE_REQUIRED" { + t.Fatalf("error code = %q body=%s", body.Error.Code, rec.Body.String()) + } + if !strings.Contains(body.Error.Message, "车辆或时间范围") { + t.Fatalf("error message should explain required scope: %s", rec.Body.String()) + } + }) + } +} + func TestHandlerRawFramesKeepsUnresolvedKeywordEmpty(t *testing.T) { handler := NewHandler(NewService(NewMockStore())) rec := httptest.NewRecorder() diff --git a/vehicle-data-platform/apps/api/internal/platform/service.go b/vehicle-data-platform/apps/api/internal/platform/service.go index e419868a..b599f849 100644 --- a/vehicle-data-platform/apps/api/internal/platform/service.go +++ b/vehicle-data-platform/apps/api/internal/platform/service.go @@ -2,6 +2,7 @@ package platform import ( "context" + "errors" "math" "net/url" "sort" @@ -56,6 +57,23 @@ type Service struct { runtime RuntimeInfo } +type clientError struct { + Code string + Message string +} + +func (e clientError) Error() string { + return e.Message +} + +func asClientError(err error) (clientError, bool) { + var target clientError + if errors.As(err, &target) { + return target, true + } + return clientError{}, false +} + var canonicalVehicleProtocols = []string{"GB32960", "JT808", "YUTONG_MQTT"} func completeProtocolStats(stats []ProtocolStat) []ProtocolStat { @@ -737,6 +755,12 @@ func (s *Service) RawFrames(ctx context.Context, query RawFrameQuery) (Page[RawF if query.Limit <= 0 || query.Limit > 500 { query.Limit = 100 } + if query.IncludeFields && !rawFrameQueryHasNarrowScope(query) { + return Page[RawFrameRow]{}, clientError{ + Code: "RAW_FRAME_SCOPE_REQUIRED", + Message: "RAW 解析字段查询需要提供车辆或时间范围", + } + } if strings.TrimSpace(query.VIN) == "" { query.VIN = query.Keyword } @@ -750,6 +774,14 @@ func (s *Service) RawFrames(ctx context.Context, query RawFrameQuery) (Page[RawF return s.store.RawFrames(ctx, query) } +func rawFrameQueryHasNarrowScope(query RawFrameQuery) bool { + return strings.TrimSpace(query.VIN) != "" || + strings.TrimSpace(query.Keyword) != "" || + strings.TrimSpace(query.DateFrom) != "" || + strings.TrimSpace(query.DateTo) != "" || + len(query.Fields) > 0 +} + func (s *Service) MileageSummary(ctx context.Context, query url.Values) (MileageSummary, error) { resolvedQuery, err := s.resolveVehicleQuery(ctx, query) if err != nil {