feat: 完善每日氢耗明细导出与质量标记
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: 车辆数据开放平台 API
|
||||
version: 1.6.0
|
||||
version: 1.7.0
|
||||
license:
|
||||
name: Proprietary
|
||||
description: |
|
||||
@@ -20,7 +20,7 @@ paths:
|
||||
post:
|
||||
tags: [合作方数据接口]
|
||||
summary: 查询车辆单日用氢量
|
||||
description: plateNumbers 省略或传空数组时,返回该应用在查询自然日有效授权的全部车辆。
|
||||
description: plateNumbers 省略或传空数组时,返回该应用在查询自然日有效授权的全部车辆。SUSPECT 计算结果保留用氢量并以 DATA_ANOMALY 输出,同时返回质量原因。
|
||||
operationId: queryDailyHydrogenConsumption
|
||||
security:
|
||||
- AppKeyAuth: []
|
||||
@@ -619,7 +619,7 @@ components:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
description: 单日用氢量,kg;无数据时为 null
|
||||
description: 单日用氢量,kg;OK 和 SUSPECT 计算结果均返回数值,无数据时为 null
|
||||
calculationPhase:
|
||||
type: string
|
||||
enum: [PRELIMINARY, FINAL]
|
||||
@@ -627,6 +627,13 @@ components:
|
||||
algorithmVersion:
|
||||
type: string
|
||||
description: 氢耗计算算法版本
|
||||
qualityStatus:
|
||||
type: string
|
||||
enum: [OK, SUSPECT, NO_DATA]
|
||||
description: 氢耗计算质量状态
|
||||
qualityReason:
|
||||
type: string
|
||||
description: 质量判定原因;SUSPECT 或 NO_DATA 时供调用方解释和审计
|
||||
status:
|
||||
$ref: '#/components/schemas/DataStatus'
|
||||
MileageResult:
|
||||
|
||||
@@ -51,6 +51,8 @@ type HydrogenResult struct {
|
||||
HydrogenConsumptionKg *float64 `json:"hydrogenConsumptionKg"`
|
||||
CalculationPhase string `json:"calculationPhase,omitempty"`
|
||||
AlgorithmVersion string `json:"algorithmVersion,omitempty"`
|
||||
QualityStatus string `json:"qualityStatus,omitempty"`
|
||||
QualityReason string `json:"qualityReason,omitempty"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
@@ -288,6 +290,7 @@ type DailyHydrogen struct {
|
||||
ConsumptionKg float64
|
||||
SampleCount int
|
||||
QualityStatus string
|
||||
QualityReason string
|
||||
CalculationPhase string
|
||||
AlgorithmVersion string
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ func (r *MySQLRepository) DailyHydrogen(ctx context.Context, vins []string, date
|
||||
return map[string]DailyHydrogen{}, nil
|
||||
}
|
||||
query, args := inQuery(`
|
||||
SELECT vin,DATE_FORMAT(stat_date,'%Y-%m-%d'),consumption_kg,sample_count,quality_status,
|
||||
SELECT vin,DATE_FORMAT(stat_date,'%Y-%m-%d'),consumption_kg,sample_count,quality_status,quality_reason,
|
||||
calculation_phase,algorithm_version
|
||||
FROM vehicle_open_daily_energy
|
||||
WHERE energy_type='HYDROGEN' AND stat_date=? AND vin IN (%s)`, date, vins)
|
||||
@@ -324,7 +324,7 @@ WHERE energy_type='HYDROGEN' AND stat_date=? AND vin IN (%s)`, date, vins)
|
||||
out := make(map[string]DailyHydrogen, len(vins))
|
||||
for rows.Next() {
|
||||
var value DailyHydrogen
|
||||
if err := rows.Scan(&value.VIN, &value.Date, &value.ConsumptionKg, &value.SampleCount, &value.QualityStatus, &value.CalculationPhase, &value.AlgorithmVersion); err != nil {
|
||||
if err := rows.Scan(&value.VIN, &value.Date, &value.ConsumptionKg, &value.SampleCount, &value.QualityStatus, &value.QualityReason, &value.CalculationPhase, &value.AlgorithmVersion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[value.VIN] = value
|
||||
|
||||
@@ -394,10 +394,17 @@ func (s *Service) QueryHydrogen(ctx context.Context, appKey, traceID string, req
|
||||
if value, ok := values[vehicle.VIN]; ok {
|
||||
item.CalculationPhase = value.CalculationPhase
|
||||
item.AlgorithmVersion = value.AlgorithmVersion
|
||||
if strings.EqualFold(value.QualityStatus, "OK") {
|
||||
item.QualityStatus = value.QualityStatus
|
||||
item.QualityReason = value.QualityReason
|
||||
switch {
|
||||
case strings.EqualFold(value.QualityStatus, "OK"):
|
||||
consumption := round3(value.ConsumptionKg)
|
||||
item.HydrogenConsumptionKg = &consumption
|
||||
item.Status = StatusNormal
|
||||
case strings.EqualFold(value.QualityStatus, "SUSPECT"):
|
||||
consumption := round3(value.ConsumptionKg)
|
||||
item.HydrogenConsumptionKg = &consumption
|
||||
item.Status = StatusDataAnomaly
|
||||
}
|
||||
}
|
||||
results = append(results, item)
|
||||
|
||||
@@ -294,6 +294,32 @@ func TestExternalHydrogenAndMileageQueriesPreserveRequestedVehicles(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalHydrogenQueryPublishesSuspectCalculationAsDataAnomaly(t *testing.T) {
|
||||
repository := &fakeRepository{
|
||||
app: AppCredential{ID: 7, Name: "partner"},
|
||||
vehicles: map[string]AuthorizedVehicle{
|
||||
"粤A12345": {VIN: "LTEST32960VIN0001", Plate: "粤A12345"},
|
||||
},
|
||||
hydrogen: map[string]DailyHydrogen{
|
||||
"LTEST32960VIN0001": {
|
||||
VIN: "LTEST32960VIN0001", ConsumptionKg: 4.491, QualityStatus: "SUSPECT",
|
||||
QualityReason: "燃料电池未工作期间持续压降", CalculationPhase: "FINAL", AlgorithmVersion: trustedHydrogenAlgorithmVersion,
|
||||
},
|
||||
},
|
||||
}
|
||||
service := NewService(repository)
|
||||
service.now = func() time.Time { return time.Date(2026, 8, 26, 12, 0, 0, 0, time.FixedZone("CST", 8*3600)) }
|
||||
result, err := service.QueryHydrogen(context.Background(), "0123456789abcdef0123456789abcdef", "trace-suspect", QueryRequest{
|
||||
PlateNumbers: []string{"粤A12345"}, Date: "2026-08-26",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result) != 1 || result[0].HydrogenConsumptionKg == nil || *result[0].HydrogenConsumptionKg != 4.491 || result[0].Status != StatusDataAnomaly || result[0].QualityStatus != "SUSPECT" || result[0].QualityReason == "" {
|
||||
t.Fatalf("suspect hydrogen result = %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDailyQueriesWithoutPlatesReturnAllAuthorizedVehicles(t *testing.T) {
|
||||
repository := &fakeRepository{
|
||||
app: AppCredential{ID: 7, Name: "partner"},
|
||||
|
||||
@@ -933,16 +933,18 @@ func replaceHydrogenDailyStats(ctx context.Context, db *sql.DB, date string, sco
|
||||
INSERT INTO vehicle_open_daily_energy(
|
||||
vin,stat_date,energy_type,source_endpoint,consumption_kg,unit,first_mass_kg,last_mass_kg,
|
||||
sample_count,refuel_count,quality_status,quality_reason,calculated_at,
|
||||
refuel_amount_kg,
|
||||
raw_consumption_kg,battery_soc_delta_pct,battery_discharge_kwh,battery_equivalent_kg,
|
||||
soc_balanced_consumption_kg,mixed_mileage_km,pure_electric_mileage_km,
|
||||
consumption_kg_per_100km,soc_balanced_kg_per_100km,charge_count,valid_segment_count,
|
||||
consumption_kg_per_100km,soc_balanced_kg_per_100km,charge_count,charge_energy_kwh,valid_segment_count,
|
||||
invalid_segment_count,algorithm_version,calculation_phase,parameter_json,evidence_json
|
||||
) VALUES(?,?,'HYDROGEN',?,?,'kg',?,?,?,?,?,?,NOW(3),?,?,?,?,?,?,?,?,?,?,?,?,?,'FINAL',?,?)`,
|
||||
) VALUES(?,?,'HYDROGEN',?,?,'kg',?,?,?,?,?,?,NOW(3),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'FINAL',?,?)`,
|
||||
stat.VIN, stat.Date, stat.Source, stat.ConsumptionKg, stat.FirstMassKg, stat.LastMassKg,
|
||||
stat.SampleCount, stat.RefuelCount, stat.QualityStatus, stat.QualityReason,
|
||||
stat.RefuelAmountKg,
|
||||
stat.ConsumptionKg, stat.BatterySOCDeltaPct, stat.BatteryDischargeKWh, stat.BatteryEquivalentKg,
|
||||
stat.SOCBalancedConsumptionKg, stat.MixedMileageKm, stat.PureElectricMileageKm,
|
||||
stat.ConsumptionKgPer100Km, stat.SOCBalancedKgPer100Km, stat.ChargeCount, stat.QualifiedSegmentCount,
|
||||
stat.ConsumptionKgPer100Km, stat.SOCBalancedKgPer100Km, stat.ChargeCount, stat.ChargeEnergyKWh, stat.QualifiedSegmentCount,
|
||||
stat.InvalidSegmentCount, stat.CalculationParameters.AlgorithmVersion, parameterJSON, evidenceJSON,
|
||||
); err != nil {
|
||||
return err
|
||||
|
||||
@@ -355,8 +355,8 @@ func TestReplaceHydrogenDailyStatsForVINDeletesOnlyScopedVehicle(t *testing.T) {
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO vehicle_open_daily_energy(")).
|
||||
WithArgs(vin, "2026-08-12", "source-a", 8.691, 7.64, 10.515, 2803, 2, "OK", "",
|
||||
8.691, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 0.0, 0.0,
|
||||
sqlmock.AnyArg(), sqlmock.AnyArg(), 0, 0, 0, "", sqlmock.AnyArg(), sqlmock.AnyArg()).
|
||||
0.0, 8.691, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 0.0, 0.0,
|
||||
sqlmock.AnyArg(), sqlmock.AnyArg(), 0, 0.0, 0, 0, "", sqlmock.AnyArg(), sqlmock.AnyArg()).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
err = ReplaceHydrogenDailyStatsForVIN(context.Background(), db, "2026-08-12", vin, []HydrogenDailyStat{{
|
||||
@@ -388,9 +388,9 @@ func TestReplaceHydrogenDailyStatsForVINsDeletesOnlyScopedBrandVehicles(t *testi
|
||||
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,
|
||||
stat.SampleCount, 0, stat.QualityStatus, "", 0.0, 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()).
|
||||
sqlmock.AnyArg(), sqlmock.AnyArg(), 0, 0.0, 0, 0, "", sqlmock.AnyArg(), sqlmock.AnyArg()).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
@@ -425,8 +425,8 @@ func TestReplaceHydrogenDailyStatsAndSeedStreamIsAtomic(t *testing.T) {
|
||||
WithArgs("2026-08-19").WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO vehicle_open_daily_energy(")).
|
||||
WithArgs(vin, "2026-08-19", "source-a", 2.6, 12.0, 9.4, 500, 1, "OK", "",
|
||||
2.6, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 0.0, 0.0,
|
||||
sqlmock.AnyArg(), sqlmock.AnyArg(), 0, 2, 0, "", sqlmock.AnyArg(), sqlmock.AnyArg()).
|
||||
0.0, 2.6, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 0.0, 0.0,
|
||||
sqlmock.AnyArg(), sqlmock.AnyArg(), 0, 0.0, 2, 0, "", sqlmock.AnyArg(), sqlmock.AnyArg()).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO vehicle_open_hydrogen_segment_stream_state(")).
|
||||
WithArgs(vin, "2026-08-19", "source-a", 2.6, 2.6, 500, 1, 0, 460, 2, 9.4, last.ObservedAt, "", sqlmock.AnyArg(), "OK", "").
|
||||
@@ -469,8 +469,8 @@ func TestReplaceHydrogenDailyStatsAndSeedStreamSkipsMissingWatermark(t *testing.
|
||||
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()).
|
||||
0.0, 0.0, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 0.0, 0.0,
|
||||
sqlmock.AnyArg(), sqlmock.AnyArg(), 0, 0.0, 0, 0, "", sqlmock.AnyArg(), sqlmock.AnyArg()).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user