功能:扩展开放平台氢耗溯源与合作站数据

This commit is contained in:
lingniu
2026-09-02 14:05:34 +08:00
parent 8759668d39
commit c6875fbb98
33 changed files with 5692 additions and 475 deletions
@@ -75,13 +75,62 @@ func TestAuthorizedVehiclesWithoutPlateFilterReturnsAllGrantedVehicles(t *testin
}
}
func TestDailyMileageReturnsDailyAndSameProtocolEndTotal(t *testing.T) {
func TestHydrogenStationsUsesNewMasterAndExcelSinopecRoster(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
mock.ExpectQuery("SELECT MAX\\(selected.latest_event_time\\).*FROM vehicle_daily_mileage m\\s+WHERE m.stat_date BETWEEN \\? AND \\?.*m.latest_total_mileage_km>=0.*m.daily_mileage_km>=0").
mock.ExpectQuery("cooperation_start_date.*FROM ln_asset_management\\.new_hydrogen_site n.*common_district province_region.*common_district city_region.*ORDER BY province_region\\.NAME,city_region\\.NAME,n\\.site_name,n\\.id").
WithArgs("湖北省", "湖北省", "武汉市", "武汉市").
WillReturnRows(sqlmock.NewRows([]string{
"id", "name", "short_name", "address", "longitude", "latitude", "province", "city", "district", "cooperative", "contact_person", "contact_phone", "unit_price", "monthly_hydrogen_kg", "total_hydrogen_kg",
}).
AddRow("1", "武汉群力加油站", "中国石化群力加油站", "地址一", 114.1, 30.2, "湖北省", "武汉市", "", false, "张工", "13800000001", 12.5, 12.5, 100.5).
AddRow("2", "已有合作日期站", "", "地址二", 120.1, 31.2, "上海市", "上海市", "", true, "李工", "13800000002", 13.0, 8.5, 80.5).
AddRow("3", "普通外部站", "", "地址三", 121.1, 32.2, "江苏省", "南京市", "", false, "", "", 0, 0, 0))
cooperateOnly := true
stations, err := NewMySQLRepository(db).HydrogenStations(context.Background(), HydrogenStationRequest{Province: "湖北省", City: "武汉市", CooperateOnly: &cooperateOnly})
if err != nil {
t.Fatal(err)
}
if len(stations) != 2 {
t.Fatalf("expected roster and canonical cooperative stations, got %#v", stations)
}
if stations[0].Name != "武汉群力加油站" || !stations[0].Cooperative {
t.Fatalf("Excel SINOPEC station was not classified as cooperative: %#v", stations[0])
}
if stations[1].Name != "已有合作日期站" || !stations[1].Cooperative {
t.Fatalf("canonical cooperative station was lost: %#v", stations[1])
}
if stations[0].ContactPerson != "张工" || stations[0].ContactPhone != "13800000001" || stations[0].UnitPrice != 12.5 {
t.Fatalf("partner station contact and price were not mapped: %#v", stations[0])
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestExcelSinopecRosterMatchesEitherExcelNameColumn(t *testing.T) {
for _, name := range []string{"中国石化青岛炼油化工有限责任公司", "青岛中石化加氢站", "南京溧水柘塘东站", "南京中石化溧水加氢站", "中国石化樟坑加油加氢站"} {
if !isExcelSinopecStation(name) {
t.Fatalf("expected roster match for %q", name)
}
}
if isExcelSinopecStation("普通外部加氢站") {
t.Fatal("non-roster station must not be classified as cooperative")
}
}
func TestDailyMileageReturnsDailyAndAuthoritativeSameProtocolEndTotal(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
mock.ExpectQuery("COALESCE\\(m.day_end_total_mileage_km,m.latest_total_mileage_km\\).*FROM vehicle_daily_mileage m\\s+WHERE m.stat_date BETWEEN \\? AND \\?.*COALESCE\\(m.day_end_total_mileage_km,m.latest_total_mileage_km\\)>=0.*m.daily_mileage_km>=0").
WithArgs("2026-07-21", "2026-07-21", "LTEST32960VIN0001", "LTEST32960VIN0002").
WillReturnRows(sqlmock.NewRows([]string{"vin", "date", "protocol", "daily_mileage_km", "latest_total_mileage_km", "data_time", "updated_at"}).
AddRow("LTEST32960VIN0001", "2026-07-21", "GB32960", 101.235, 12345.679, "2026-07-21T23:58:45+08:00", "2026-07-22T05:10:00+08:00"))
@@ -185,6 +234,30 @@ func TestTotalMileageUsesProtocolPriorityAndLatestRecordAtOrBeforeTime(t *testin
}
}
func TestStationaryLocationPointsUsesTimeVINAndSmallBoundingBox(t *testing.T) {
mysqlDB, _, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer mysqlDB.Close()
tdDB, tdMock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer tdDB.Close()
start := time.Date(2026, 8, 6, 10, 0, 0, 0, time.FixedZone("CST", 8*3600))
tdMock.ExpectQuery("FROM lingniu_vehicle_ts.vehicle_locations.*ts>='2026-08-06T10:00:00\\+08:00'.*vin IN \\('LTEST32960VIN0001'\\).*speed_kmh BETWEEN 0 AND 3\\.000.*ORDER BY vin,ts ASC,protocol ASC").
WillReturnRows(sqlmock.NewRows([]string{"ts", "vin", "protocol", "longitude", "latitude", "speed_kmh"}).
AddRow(start.UnixMilli(), "LTEST32960VIN0001", "GB32960", 120.000001, 30.000001, 0.2))
points, err := NewMySQLRepository(mysqlDB).WithTDengine(tdDB, "lingniu_vehicle_ts").StationaryLocationPoints(context.Background(), []string{"LTEST32960VIN0001"}, start, start.Add(time.Hour), 120, 30, 5, 3)
if err != nil || len(points) != 1 || points[0].SpeedKmh != 0.2 || !points[0].ObservedAt.Equal(start) {
t.Fatalf("points=%#v err=%v", points, err)
}
if err := tdMock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestRealtimeVehiclesAnyFreshProtocolKeepsSelectedSourceOnline(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
@@ -192,19 +265,20 @@ func TestRealtimeVehiclesAnyFreshProtocolKeepsSelectedSourceOnline(t *testing.T)
}
defer db.Close()
now := time.Date(2026, 8, 3, 21, 37, 30, 0, time.Local)
dayStart := time.Date(2026, 8, 3, 0, 0, 0, 0, time.Local)
vin := "LTEST32960VIN0001"
mock.ExpectQuery("SELECT l.vin,l.protocol.*FROM vehicle_realtime_location").
WithArgs(vin, now.Add(-10*time.Minute)).
WillReturnRows(sqlmock.NewRows([]string{"vin", "protocol", "longitude", "latitude", "speed_kmh", "total_mileage_km", "updated_at"}).
AddRow(vin, "GB32960", 120.1, 30.2, 0, 1000, now.Add(-2*time.Minute)).
AddRow(vin, "JT808", 120.2, 30.3, 10, 0, now.Add(-20*time.Second)))
WithArgs(dayStart, vin, now.Add(-10*time.Minute)).
WillReturnRows(sqlmock.NewRows([]string{"vin", "protocol", "longitude", "latitude", "speed_kmh", "soc_percent", "total_mileage_km", "updated_at", "active_today"}).
AddRow(vin, "GB32960", 120.1, 30.2, 0, 86.5, 1000, now.Add(-2*time.Minute), true).
AddRow(vin, "JT808", 120.2, 30.3, 10, nil, 0, now.Add(-20*time.Second), true))
points, err := NewMySQLRepository(db).RealtimeVehicles(context.Background(), []string{vin}, now)
if err != nil {
t.Fatal(err)
}
point := points[vin]
if point.Protocol != "GB32960" || !point.Online || !point.ObservedAt.Equal(now.Add(-2*time.Minute)) {
if point.Protocol != "GB32960" || point.SOCPercent == nil || *point.SOCPercent != 86.5 || !point.Online || !point.ActiveToday || !point.ObservedAt.Equal(now.Add(-2*time.Minute)) {
t.Fatalf("selected source and aggregate online state mismatch: %#v", point)
}
if err := mock.ExpectationsWereMet(); err != nil {