功能:扩展开放平台氢耗溯源与合作站数据
This commit is contained in:
@@ -543,6 +543,7 @@ func TestHistoryPreferencesPersistPerAccountAndSupplyExportRetention(t *testing.
|
||||
|
||||
type countingStore struct {
|
||||
*MockStore
|
||||
rawMu sync.Mutex
|
||||
vehiclesCalls int
|
||||
vehicleRealtimeCalls int
|
||||
overviewBatchCalls int
|
||||
@@ -555,6 +556,17 @@ type countingStore struct {
|
||||
lastMileageStatisticsQuery url.Values
|
||||
}
|
||||
|
||||
type projectionCountingStore struct {
|
||||
*countingStore
|
||||
anchors Page[RawFrameRow]
|
||||
projectionQueries []RawFrameQuery
|
||||
}
|
||||
|
||||
func (s *projectionCountingStore) LatestRealtimeFrames(_ context.Context, query RawFrameQuery) (Page[RawFrameRow], error) {
|
||||
s.projectionQueries = append(s.projectionQueries, query)
|
||||
return s.anchors, nil
|
||||
}
|
||||
|
||||
type hydrogenMetricsStore struct{ *MockStore }
|
||||
|
||||
func (s *hydrogenMetricsStore) DailyMileage(context.Context, url.Values) (Page[DailyMileageRow], error) {
|
||||
@@ -572,15 +584,26 @@ func (s *hydrogenMetricsStore) MileageSummary(context.Context, url.Values) (Mile
|
||||
func (s *hydrogenMetricsStore) MileageStatistics(context.Context, url.Values) (MileageStatistics, error) {
|
||||
consumption, rate := 3.1, 5.5
|
||||
return MileageStatistics{
|
||||
PeriodMileageKm: 88.7, PeriodPureHydrogenMileageKm: 56.2, HydrogenMatchedMileageKm: 88.7, HydrogenDataDays: 1,
|
||||
PeriodMileageKm: 88.7, PeriodPureHydrogenMileageKm: 56.2, HydrogenMatchedMileageKm: 56.2, HydrogenDataDays: 1,
|
||||
PeriodHydrogenConsumptionKg: consumption, HydrogenConsumptionKgPer100Km: &rate,
|
||||
Trend: []MileageTrendPoint{{
|
||||
Date: "2026-07-13", MileageKm: 88.7, PureHydrogenMileageKm: 56.2, HydrogenMatchedMileageKm: 88.7, HydrogenDataDays: 1,
|
||||
Date: "2026-07-13", MileageKm: 88.7, PureHydrogenMileageKm: 56.2, HydrogenMatchedMileageKm: 56.2, HydrogenDataDays: 1,
|
||||
HydrogenConsumptionKg: &consumption, HydrogenConsumptionKgPer100Km: &rate,
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *hydrogenMetricsStore) HydrogenDailyEvidence(context.Context, string, string) (HydrogenDailyEvidence, bool, error) {
|
||||
return HydrogenDailyEvidence{
|
||||
VIN: "LB9A32A24R0LS1426",
|
||||
Date: "2026-07-13",
|
||||
AlgorithmVersion: "PRESSURE_NIST_SEGMENT_MEDIAN_5_SOC_BALANCE_V2",
|
||||
Intervals: []HydrogenIntervalEvidenceRow{{
|
||||
Index: 1, StartEventID: "event-start", EndEventID: "event-end", QualityStatus: "OK",
|
||||
}},
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func newCountingStore() *countingStore {
|
||||
return &countingStore{MockStore: NewMockStore()}
|
||||
}
|
||||
@@ -622,6 +645,27 @@ func TestHydrogenConsumptionMetricsRemainAvailableToInternalAccounts(t *testing.
|
||||
if len(daily.Items) != 1 || daily.Items[0].PureHydrogenMileageKm != 56.2 || daily.Items[0].HydrogenConsumptionKg == nil || *daily.Items[0].HydrogenConsumptionKg != 3.1 {
|
||||
t.Fatalf("internal daily mileage lost hydrogen metrics: %+v", daily.Items)
|
||||
}
|
||||
evidence, err := service.HydrogenDailyEvidence(exportAdminContext(), "LB9A32A24R0LS1426", "2026-07-13")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if evidence.AlgorithmVersion == "" || len(evidence.Intervals) != 1 || evidence.Intervals[0].StartEventID != "event-start" {
|
||||
t.Fatalf("internal hydrogen evidence is incomplete: %+v", evidence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHydrogenDailyEvidenceIsDeniedToCustomerAccounts(t *testing.T) {
|
||||
service := NewService(&hydrogenMetricsStore{MockStore: NewMockStore()})
|
||||
validFrom := time.Date(2026, 7, 12, 0, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*60*60))
|
||||
customer := WithPrincipal(context.Background(), Principal{
|
||||
Name: "业务客户", Role: "customer", UserType: "customer",
|
||||
VehicleVINs: []string{"LB9A32A24R0LS1426"},
|
||||
VehicleGrants: []VehicleGrant{{VIN: "LB9A32A24R0LS1426", ValidFrom: validFrom}},
|
||||
})
|
||||
_, err := service.HydrogenDailyEvidence(customer, "LB9A32A24R0LS1426", "2026-07-13")
|
||||
if clientErr, ok := asClientError(err); !ok || clientErr.Code != "VEHICLE_PERMISSION_DENIED" {
|
||||
t.Fatalf("customer hydrogen evidence should be forbidden, err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *countingStore) Vehicles(ctx context.Context, query url.Values) (Page[VehicleRow], error) {
|
||||
@@ -643,7 +687,9 @@ func (s *countingStore) HistoryLocationsFromTDengine(ctx context.Context, query
|
||||
}
|
||||
|
||||
func (s *countingStore) RawFrames(ctx context.Context, query RawFrameQuery) (Page[RawFrameRow], error) {
|
||||
s.rawMu.Lock()
|
||||
s.rawFrameQueries = append(s.rawFrameQueries, query)
|
||||
s.rawMu.Unlock()
|
||||
return s.MockStore.RawFrames(ctx, query)
|
||||
}
|
||||
|
||||
@@ -692,6 +738,66 @@ func TestVehicleDetailSkipsUnneededTDengineCounts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVehicleDetailUsesPersistentProjectionAsBoundedHistoricalAnchor(t *testing.T) {
|
||||
base := newCountingStore()
|
||||
store := &projectionCountingStore{
|
||||
countingStore: base,
|
||||
anchors: Page[RawFrameRow]{Items: []RawFrameRow{
|
||||
{ID: "snapshot-gb", VIN: "LB9A32A24R0LS1426", Protocol: "GB32960", FrameType: "realtime_projection", DeviceTime: "2025-03-04T12:00:00+08:00", ServerTime: "2025-03-04T12:00:01+08:00", ParsedFields: map[string]any{"gb32960.vehicle.speed_kmh": 0}},
|
||||
{ID: "snapshot-jt", VIN: "LB9A32A24R0LS1426", Protocol: "JT808", FrameType: "realtime_projection", DeviceTime: "2025-02-03T10:00:00+08:00", ServerTime: "2025-02-03T10:00:01+08:00", ParsedFields: map[string]any{"jt808.location.speed_kmh": 0}},
|
||||
{ID: "snapshot-mqtt", VIN: "LB9A32A24R0LS1426", Protocol: "YUTONG_MQTT", FrameType: "realtime_projection", DeviceTime: "2025-01-01T08:00:00+08:00", ServerTime: "2025-01-01T08:00:01+08:00", ParsedFields: map[string]any{"yutong_mqtt.data.speed_kmh": 0}},
|
||||
}},
|
||||
}
|
||||
detail, err := NewService(store).VehicleDetail(exportAdminContext(), "LB9A32A24R0LS1426", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(store.projectionQueries) != 1 || store.projectionQueries[0].VIN != "LB9A32A24R0LS1426" {
|
||||
t.Fatalf("projection queries = %+v", store.projectionQueries)
|
||||
}
|
||||
if len(store.rawFrameQueries) != 3 {
|
||||
t.Fatalf("bounded protocol RAW queries = %+v", store.rawFrameQueries)
|
||||
}
|
||||
for _, query := range store.rawFrameQueries {
|
||||
if query.Protocol == "" || query.DateFrom == "" || query.DateTo == "" || !query.SkipCount {
|
||||
t.Fatalf("RAW preview must be protocol-specific, time-bounded, and count-free: %+v", query)
|
||||
}
|
||||
if strings.HasPrefix(query.DateFrom, "2026-") || strings.HasPrefix(query.DateTo, "2026-") {
|
||||
t.Fatalf("offline projection should anchor its actual historical day, not today: %+v", query)
|
||||
}
|
||||
if !strings.Contains(query.DateFrom, "+08:00") || !strings.Contains(query.DateTo, "+08:00") {
|
||||
t.Fatalf("bounded RAW preview should preserve the platform timezone: %+v", query)
|
||||
}
|
||||
}
|
||||
if len(detail.Raw.Items) != 3 {
|
||||
t.Fatalf("bounded preview should preserve every latest protocol row: %+v", detail.Raw)
|
||||
}
|
||||
projectionFallbacks := 0
|
||||
for _, item := range detail.Raw.Items {
|
||||
if item.FrameType == "realtime_projection" {
|
||||
projectionFallbacks++
|
||||
}
|
||||
}
|
||||
if projectionFallbacks != 2 {
|
||||
t.Fatalf("missing projection fallbacks for protocols without RAW test rows: %+v", detail.Raw.Items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVehicleDetailDoesNotFallBackToUnboundedRAWWhenProjectionIsEmpty(t *testing.T) {
|
||||
base := newCountingStore()
|
||||
store := &projectionCountingStore{countingStore: base, anchors: Page[RawFrameRow]{Items: []RawFrameRow{}}}
|
||||
detail, err := NewService(store).VehicleDetail(exportAdminContext(), "LB9A32A24R0LS1426", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(store.rawFrameQueries) != 0 {
|
||||
t.Fatalf("empty durable projection must not trigger an unbounded RAW scan: %+v", store.rawFrameQueries)
|
||||
}
|
||||
if detail.Raw.Items == nil || len(detail.Raw.Items) != 0 || detail.Raw.Total != 0 {
|
||||
t.Fatalf("empty projection should return a stable empty preview: %+v", detail.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *countingStore) MileageStatistics(ctx context.Context, query url.Values) (MileageStatistics, error) {
|
||||
s.lastMileageStatisticsQuery = cloneValues(query)
|
||||
return s.MockStore.MileageStatistics(ctx, query)
|
||||
|
||||
Reference in New Issue
Block a user