功能:扩展开放平台氢耗溯源与合作站数据
This commit is contained in:
@@ -12,27 +12,32 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func float64Pointer(value float64) *float64 { return &value }
|
||||
|
||||
type fakeRepository struct {
|
||||
app AppCredential
|
||||
authErr error
|
||||
vehicles map[string]AuthorizedVehicle
|
||||
hydrogen map[string]DailyHydrogen
|
||||
mileage map[string]DailyMileage
|
||||
priorMileage map[string]DailyMileage
|
||||
authorizedVIN bool
|
||||
totalMileage *TotalMileagePoint
|
||||
realtime map[string]RealtimeVehiclePoint
|
||||
stations []HydrogenStation
|
||||
audits []string
|
||||
createdHash [sha256.Size]byte
|
||||
createdPrefix string
|
||||
requestedPlates []string
|
||||
dailyVINs []string
|
||||
dailyProtocols []string
|
||||
rangeVINs []string
|
||||
priorVINs []string
|
||||
priorCalls int
|
||||
snapshot MileageSnapshot
|
||||
app AppCredential
|
||||
authErr error
|
||||
vehicles map[string]AuthorizedVehicle
|
||||
hydrogen map[string]DailyHydrogen
|
||||
mileage map[string]DailyMileage
|
||||
priorMileage map[string]DailyMileage
|
||||
rollbacks map[string]bool
|
||||
authorizedVIN bool
|
||||
totalMileage *TotalMileagePoint
|
||||
realtime map[string]RealtimeVehiclePoint
|
||||
stationary []StationaryLocationPoint
|
||||
stations []HydrogenStation
|
||||
audits []string
|
||||
createdHash [sha256.Size]byte
|
||||
createdPrefix string
|
||||
requestedPlates []string
|
||||
dailyVINs []string
|
||||
dailyProtocols []string
|
||||
rangeVINs []string
|
||||
priorVINs []string
|
||||
priorCalls int
|
||||
snapshot MileageSnapshot
|
||||
stationaryRadius float64
|
||||
}
|
||||
|
||||
func (f *fakeRepository) Authenticate(context.Context, [sha256.Size]byte, time.Time, time.Time, time.Time) (AppCredential, error) {
|
||||
@@ -64,6 +69,9 @@ func (f *fakeRepository) LatestMileageBefore(_ context.Context, vins []string, _
|
||||
f.dailyProtocols = append([]string(nil), protocols...)
|
||||
return f.priorMileage, nil
|
||||
}
|
||||
func (f *fakeRepository) MileageRollbacks(context.Context, []string, string, string, []string) (map[string]bool, error) {
|
||||
return f.rollbacks, nil
|
||||
}
|
||||
func (f *fakeRepository) CreateMileageSnapshot(_ context.Context, snapshot MileageSnapshot) error {
|
||||
f.snapshot = snapshot
|
||||
return nil
|
||||
@@ -80,6 +88,10 @@ func (f *fakeRepository) AuthorizedVIN(context.Context, uint64, string, time.Tim
|
||||
func (f *fakeRepository) TotalMileage(context.Context, string, time.Time, []string) (*TotalMileagePoint, error) {
|
||||
return f.totalMileage, nil
|
||||
}
|
||||
func (f *fakeRepository) StationaryLocationPoints(_ context.Context, _ []string, _ time.Time, _ time.Time, _ float64, _ float64, radiusMeters float64, _ float64) ([]StationaryLocationPoint, error) {
|
||||
f.stationaryRadius = radiusMeters
|
||||
return f.stationary, nil
|
||||
}
|
||||
func (f *fakeRepository) RealtimeVehicles(context.Context, []string, time.Time) (map[string]RealtimeVehiclePoint, error) {
|
||||
return f.realtime, nil
|
||||
}
|
||||
@@ -100,7 +112,7 @@ func TestRealtimeVehicleAndHydrogenStationQueries(t *testing.T) {
|
||||
"浙B67890": {VIN: "LTEST32960VIN0002", Plate: "浙B67890"},
|
||||
},
|
||||
realtime: map[string]RealtimeVehiclePoint{
|
||||
"LTEST32960VIN0001": {VIN: "LTEST32960VIN0001", Protocol: "GB32960", Longitude: 120.1, Latitude: 30.2, SpeedKmh: 42.5, TotalMileageKm: 12345.6, ObservedAt: now.Add(-30 * time.Second), Online: true},
|
||||
"LTEST32960VIN0001": {VIN: "LTEST32960VIN0001", Protocol: "GB32960", Longitude: 120.1, Latitude: 30.2, SpeedKmh: 42.5, SOCPercent: float64Pointer(78.5), TotalMileageKm: 12345.6, ObservedAt: now.Add(-30 * time.Second), Online: true},
|
||||
},
|
||||
stations: []HydrogenStation{{ID: "1", Name: "测试加氢站", Longitude: 120.2, Latitude: 30.3}},
|
||||
}
|
||||
@@ -112,7 +124,7 @@ func TestRealtimeVehicleAndHydrogenStationQueries(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(vehicles) != 2 || !vehicles[0].Online || vehicles[0].MotionStatus != "driving" || !vehicles[0].LocationAvailable {
|
||||
if len(vehicles) != 2 || !vehicles[0].Online || vehicles[0].MotionStatus != "driving" || !vehicles[0].LocationAvailable || vehicles[0].Protocol != "GB32960" || vehicles[0].SOCPercent == nil || *vehicles[0].SOCPercent != 78.5 {
|
||||
t.Fatalf("unexpected realtime vehicles: %#v", vehicles)
|
||||
}
|
||||
if vehicles[1].Status != StatusNoData || vehicles[1].MotionStatus != "offline" {
|
||||
@@ -127,6 +139,76 @@ func TestRealtimeVehicleAndHydrogenStationQueries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStationaryVehicleQueryBuildsAndRanksStayMatches(t *testing.T) {
|
||||
location := time.FixedZone("CST", 8*3600)
|
||||
start := time.Date(2026, 8, 6, 10, 0, 0, 0, location)
|
||||
repository := &fakeRepository{
|
||||
app: AppCredential{ID: 9, Name: "refuel-check"},
|
||||
vehicles: map[string]AuthorizedVehicle{
|
||||
"浙A12345": {VIN: "LTEST32960VIN0001", Plate: "浙A12345"},
|
||||
"浙B67890": {VIN: "LTEST32960VIN0002", Plate: "浙B67890"},
|
||||
},
|
||||
stationary: []StationaryLocationPoint{
|
||||
{VIN: "LTEST32960VIN0002", Protocol: "JT808", ObservedAt: start.Add(10 * time.Minute), Longitude: 120.00001, Latitude: 30.00001, SpeedKmh: 0},
|
||||
{VIN: "LTEST32960VIN0002", Protocol: "JT808", ObservedAt: start.Add(13 * time.Minute), Longitude: 120.00001, Latitude: 30.00001, SpeedKmh: 0.1},
|
||||
{VIN: "LTEST32960VIN0001", Protocol: "GB32960", ObservedAt: start.Add(time.Minute), Longitude: 120.000002, Latitude: 30.000002, SpeedKmh: 0},
|
||||
{VIN: "LTEST32960VIN0001", Protocol: "GB32960", ObservedAt: start.Add(5 * time.Minute), Longitude: 120.000002, Latitude: 30.000002, SpeedKmh: 0},
|
||||
{VIN: "LTEST32960VIN0001", Protocol: "YUTONG_MQTT", ObservedAt: start.Add(6 * time.Minute), Longitude: 120.000002, Latitude: 30.000002, SpeedKmh: 0},
|
||||
},
|
||||
}
|
||||
service := NewService(repository)
|
||||
service.now = func() time.Time { return start }
|
||||
radiusMeters := 8.0
|
||||
values, err := service.QueryStationaryVehicles(context.Background(), "0123456789abcdef0123456789abcdef", "trace-stationary", StationaryVehicleQueryRequest{
|
||||
StartTime: start.Format("2006-01-02 15:04:05"), EndTime: start.Add(time.Hour).Format("2006-01-02 15:04:05"), Longitude: 120, Latitude: 30, RadiusMeters: &radiusMeters,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(values) != 2 || values[0].VIN != "LTEST32960VIN0001" || values[0].StayDurationSeconds != 300 || values[0].MatchedSamples != 3 || values[0].SourceProtocols[0] != "GB32960" || values[0].SourceProtocols[1] != "MQTT" || values[0].MatchScore <= values[1].MatchScore {
|
||||
t.Fatalf("stationary matches=%#v", values)
|
||||
}
|
||||
if repository.stationaryRadius != radiusMeters {
|
||||
t.Fatalf("stationary radius=%v, want %v", repository.stationaryRadius, radiusMeters)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStationaryVehicleQueryRejectsInvalidWindowAndCoordinate(t *testing.T) {
|
||||
service := NewService(&fakeRepository{})
|
||||
if _, err := service.QueryStationaryVehicles(context.Background(), "0123456789abcdef0123456789abcdef", "trace", StationaryVehicleQueryRequest{StartTime: "2026-08-06 10:00:00", EndTime: "2026-08-07 10:00:01", Longitude: 120, Latitude: 30}); !errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("window error=%v", err)
|
||||
}
|
||||
if _, err := service.QueryStationaryVehicles(context.Background(), "0123456789abcdef0123456789abcdef", "trace", StationaryVehicleQueryRequest{StartTime: "2026-08-06 10:00:00", EndTime: "2026-08-06 10:10:00", Longitude: 0, Latitude: 0}); !errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("coordinate error=%v", err)
|
||||
}
|
||||
overRadius := 101.0
|
||||
if _, err := service.QueryStationaryVehicles(context.Background(), "0123456789abcdef0123456789abcdef", "trace", StationaryVehicleQueryRequest{StartTime: "2026-08-06 10:00:00", EndTime: "2026-08-06 10:10:00", Longitude: 120, Latitude: 30, RadiusMeters: &overRadius}); !errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("radius error=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStationaryVehicleQueryConvertsGCJ02Coordinate(t *testing.T) {
|
||||
service := NewService(&fakeRepository{})
|
||||
_, _, _, longitude, latitude, _, err := service.validateStationaryVehicleQuery(StationaryVehicleQueryRequest{
|
||||
StartTime: "2026-08-06 17:53:01",
|
||||
EndTime: "2026-08-06 17:55:01",
|
||||
Longitude: 121.045455,
|
||||
Latitude: 30.640433,
|
||||
CoordinateSystem: "gcj02",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if longitude < 121.0409 || longitude > 121.0411 || latitude < 30.6425 || latitude > 30.6428 {
|
||||
t.Fatalf("converted coordinate=%f,%f", longitude, latitude)
|
||||
}
|
||||
if _, _, _, _, _, _, err := service.validateStationaryVehicleQuery(StationaryVehicleQueryRequest{
|
||||
StartTime: "2026-08-06 17:53:01", EndTime: "2026-08-06 17:55:01", Longitude: 121.045455, Latitude: 30.640433, CoordinateSystem: "BD09",
|
||||
}); !errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("coordinateSystem error=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealtimeVehicleOnlineUsesAnyFreshProtocol(t *testing.T) {
|
||||
now := time.Date(2026, 8, 3, 19, 30, 0, 0, time.FixedZone("CST", 8*3600))
|
||||
repository := &fakeRepository{
|
||||
@@ -337,6 +419,28 @@ func TestMileageCarriesForwardPreviousTotalAndPreviousCalculationTime(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestMileageRollbackIsNotExposedAsNormalCarriedData(t *testing.T) {
|
||||
const vin = "LNXNEGRR1SR321395"
|
||||
repository := &fakeRepository{
|
||||
app: AppCredential{ID: 7},
|
||||
vehicles: map[string]AuthorizedVehicle{"粤A08190F": {VIN: vin, Plate: "粤A08190F"}},
|
||||
priorMileage: map[string]DailyMileage{
|
||||
vin: {VIN: vin, Date: "2026-08-05", Protocol: "GB32960", TotalMileageKm: 48233.3, DataTime: "2026-08-05T23:00:00+08:00", UpdatedAt: "2026-08-05T23:00:01+08:00"},
|
||||
},
|
||||
rollbacks: map[string]bool{dailyMileageKey(vin, "2026-08-06"): true},
|
||||
}
|
||||
service := NewService(repository)
|
||||
result, err := service.QueryMileage(context.Background(), "0123456789abcdef0123456789abcdef", "trace-rollback", QueryRequest{
|
||||
PlateNumbers: []string{"粤A08190F"}, Date: "2026-08-06",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result) != 1 || result[0].Status != StatusDataAnomaly || result[0].DataQuality == nil || *result[0].DataQuality != mileageTotalRollbackQuality || result[0].TotalMileageKm != nil || repository.priorCalls != 0 {
|
||||
t.Fatalf("rollback result=%#v priorCalls=%d", result, repository.priorCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMileageRangeUsesStableSnapshotAndDistinguishesZeroFromNoData(t *testing.T) {
|
||||
repository := &fakeRepository{
|
||||
app: AppCredential{ID: 7, Name: "partner"},
|
||||
@@ -577,7 +681,7 @@ func TestExternalHandlerUsesDocumentEnvelopeAndErrors(t *testing.T) {
|
||||
allRequest.Header.Set("Authorization", "Bearer 0123456789abcdef0123456789abcdef")
|
||||
allResponse := httptest.NewRecorder()
|
||||
handler.ServeHTTP(allResponse, allRequest)
|
||||
if allResponse.Code != http.StatusOK || !strings.Contains(allResponse.Body.String(), `"plateNumber":"粤A12345"`) {
|
||||
if allResponse.Code != http.StatusOK || !strings.Contains(allResponse.Body.String(), `"plateNumber": "粤A12345"`) {
|
||||
t.Fatalf("status=%d body=%s", allResponse.Code, allResponse.Body.String())
|
||||
}
|
||||
|
||||
@@ -585,7 +689,7 @@ func TestExternalHandlerUsesDocumentEnvelopeAndErrors(t *testing.T) {
|
||||
rangeRequest.Header.Set("Authorization", "Bearer 0123456789abcdef0123456789abcdef")
|
||||
rangeResponse := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rangeResponse, rangeRequest)
|
||||
if rangeResponse.Code != http.StatusOK || !strings.Contains(rangeResponse.Body.String(), `"snapshotId"`) || !strings.Contains(rangeResponse.Body.String(), `"nextCursor":null`) {
|
||||
if rangeResponse.Code != http.StatusOK || !strings.Contains(rangeResponse.Body.String(), `"snapshotId"`) || !strings.Contains(rangeResponse.Body.String(), `"nextCursor": null`) {
|
||||
t.Fatalf("status=%d body=%s", rangeResponse.Code, rangeResponse.Body.String())
|
||||
}
|
||||
|
||||
@@ -593,7 +697,7 @@ func TestExternalHandlerUsesDocumentEnvelopeAndErrors(t *testing.T) {
|
||||
badDate.Header.Set("Authorization", "Bearer 0123456789abcdef0123456789abcdef")
|
||||
badResponse := httptest.NewRecorder()
|
||||
handler.ServeHTTP(badResponse, badDate)
|
||||
if badResponse.Code != http.StatusBadRequest || !strings.Contains(badResponse.Body.String(), `"code":"INVALID_DATE_FORMAT"`) {
|
||||
if badResponse.Code != http.StatusBadRequest || !strings.Contains(badResponse.Body.String(), `"code": "INVALID_DATE_FORMAT"`) {
|
||||
t.Fatalf("status=%d body=%s", badResponse.Code, badResponse.Body.String())
|
||||
}
|
||||
|
||||
@@ -602,8 +706,8 @@ func TestExternalHandlerUsesDocumentEnvelopeAndErrors(t *testing.T) {
|
||||
badProtocolResponse := httptest.NewRecorder()
|
||||
handler.ServeHTTP(badProtocolResponse, badProtocol)
|
||||
if badProtocolResponse.Code != http.StatusBadRequest ||
|
||||
!strings.Contains(badProtocolResponse.Body.String(), `"message":"protocolPriority不能包含重复协议"`) ||
|
||||
!strings.Contains(badProtocolResponse.Body.String(), `"traceId":"`) {
|
||||
!strings.Contains(badProtocolResponse.Body.String(), `"message": "protocolPriority不能包含重复协议"`) ||
|
||||
!strings.Contains(badProtocolResponse.Body.String(), `"traceId": "`) {
|
||||
t.Fatalf("status=%d body=%s", badProtocolResponse.Code, badProtocolResponse.Body.String())
|
||||
}
|
||||
|
||||
@@ -611,7 +715,7 @@ func TestExternalHandlerUsesDocumentEnvelopeAndErrors(t *testing.T) {
|
||||
nullProtocol.Header.Set("Authorization", "Bearer 0123456789abcdef0123456789abcdef")
|
||||
nullProtocolResponse := httptest.NewRecorder()
|
||||
handler.ServeHTTP(nullProtocolResponse, nullProtocol)
|
||||
if nullProtocolResponse.Code != http.StatusBadRequest || !strings.Contains(nullProtocolResponse.Body.String(), `"message":"protocolPriority不能为空"`) {
|
||||
if nullProtocolResponse.Code != http.StatusBadRequest || !strings.Contains(nullProtocolResponse.Body.String(), `"message": "protocolPriority不能为空"`) {
|
||||
t.Fatalf("status=%d body=%s", nullProtocolResponse.Code, nullProtocolResponse.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user