feat: 推广 V3.5 实时氢耗并完善导出

This commit is contained in:
lingniu
2026-09-04 15:42:02 +08:00
parent a26179c8fe
commit e402eb5e60
53 changed files with 2855 additions and 320 deletions
@@ -301,6 +301,26 @@ func TestLoadHydrogenObservationVINsFiltersMissingCapacity(t *testing.T) {
}
}
func TestLoadHydrogenBrandVINsUsesExactBrandAndValidVINs(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
mock.ExpectQuery(regexp.QuoteMeta("SELECT UPPER(TRIM(vin))")).
WithArgs("现代").
WillReturnRows(sqlmock.NewRows([]string{"vin"}).
AddRow("lb9a32a25r0ls1452").
AddRow("invalid"))
vins, err := LoadHydrogenBrandVINs(context.Background(), db, " 现代 ")
if err != nil || len(vins) != 1 || vins[0] != "LB9A32A25R0LS1452" {
t.Fatalf("vins=%v err=%v", vins, err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestHydrogenObservationDateRangeForAllUsesBoundedQueries(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
@@ -351,6 +371,37 @@ func TestReplaceHydrogenDailyStatsForVINDeletesOnlyScopedVehicle(t *testing.T) {
}
}
func TestReplaceHydrogenDailyStatsForVINsDeletesOnlyScopedBrandVehicles(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
vins := []string{"LB9A32A24R0LS1376", "LB9A32A25R0LS1452"}
stat := HydrogenDailyStat{
VIN: vins[0], Date: "2026-08-12", Source: "source-a", ConsumptionKg: 2.5,
FirstMassKg: 12, LastMassKg: 9.5, SampleCount: 500, QualityStatus: "OK",
}
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta("DELETE FROM vehicle_open_daily_energy WHERE stat_date=? AND energy_type='HYDROGEN' AND vin IN (?,?)")).
WithArgs(stat.Date, vins[0], vins[1]).
WillReturnResult(sqlmock.NewResult(0, 2))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO vehicle_open_daily_energy(")).
WithArgs(stat.VIN, stat.Date, stat.Source, stat.ConsumptionKg, stat.FirstMassKg, stat.LastMassKg,
stat.SampleCount, 0, stat.QualityStatus, "", stat.ConsumptionKg,
sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 0.0, 0.0,
sqlmock.AnyArg(), sqlmock.AnyArg(), 0, 0, 0, "", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
if err := ReplaceHydrogenDailyStatsForVINs(context.Background(), db, stat.Date, vins, []HydrogenDailyStat{stat}); err != nil {
t.Fatal(err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestReplaceHydrogenDailyStatsAndSeedStreamIsAtomic(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
@@ -396,7 +447,37 @@ func TestReplaceHydrogenDailyStatsAndSeedStreamIsAtomic(t *testing.T) {
if err := json.Unmarshal(encoded, &state); err != nil {
t.Fatal(err)
}
if state["lastObservedMassKg"] != 9.4 || state["finalizedConsumptionKg"] != 2.6 || state["lastEventTime"] == "" {
if state["lastMassKg"] != 9.4 || state["finalizedHydrogenKg"] != 2.6 || state["lastEventTime"] == "" || state["algorithmVersion"] != trustedHydrogenAlgorithmVersion {
t.Fatalf("seed state=%s", encoded)
}
}
func TestReplaceHydrogenDailyStatsAndSeedStreamSkipsMissingWatermark(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
stat := HydrogenDailyStat{
VIN: "LB9A32A24R0LS1376", Date: "2026-09-04", Source: "source-a",
QualityStatus: "NO_DATA", QualityReason: "有效运行分界点不足2条",
}
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta("DELETE FROM vehicle_open_daily_energy WHERE stat_date=? AND energy_type='HYDROGEN'")).
WithArgs("2026-09-04").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(regexp.QuoteMeta("DELETE FROM vehicle_open_hydrogen_segment_stream_state WHERE stat_date=?")).
WithArgs("2026-09-04").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO vehicle_open_daily_energy(")).
WithArgs(stat.VIN, stat.Date, stat.Source, 0.0, 0.0, 0.0, 0, 0, stat.QualityStatus, stat.QualityReason,
0.0, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 0.0, 0.0,
sqlmock.AnyArg(), sqlmock.AnyArg(), 0, 0, 0, "", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
if err := ReplaceHydrogenDailyStatsAndSeedStream(context.Background(), db, stat.Date, []HydrogenDailyStat{stat}); err != nil {
t.Fatal(err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}