功能:完善里程边界与氢耗流式统计

This commit is contained in:
lingniu
2026-09-02 14:05:04 +08:00
parent 4201878c35
commit 8759668d39
23 changed files with 1638 additions and 159 deletions
+127 -11
View File
@@ -29,6 +29,7 @@ type config struct {
DateFrom string
DateTo string
Protocols []envelope.Protocol
VINs []string
Method string
Limit int
DryRun bool
@@ -39,6 +40,8 @@ type config struct {
GPSFallback bool
ProgressEvery int64
BaselineLookback int
ListHydrogenVINs bool
ListActiveVINs bool
Location *time.Location
}
@@ -117,6 +120,21 @@ func main() {
}
ctx := context.Background()
mysqlDB, err := sql.Open("mysql", cfg.MySQLDSN)
if err != nil {
fail("open mysql", err)
}
defer mysqlDB.Close()
if err := mysqlDB.PingContext(ctx); err != nil {
fail("ping mysql", err)
}
if cfg.ListHydrogenVINs {
if err := printActiveHydrogenVINs(ctx, mysqlDB); err != nil {
fail("list active hydrogen vins", err)
}
return
}
td, err := sql.Open(cfg.TDengineDriver, cfg.TDengineDSN)
if err != nil {
fail("open tdengine", err)
@@ -125,14 +143,11 @@ func main() {
if err := td.PingContext(ctx); err != nil {
fail("ping tdengine", err)
}
mysqlDB, err := sql.Open("mysql", cfg.MySQLDSN)
if err != nil {
fail("open mysql", err)
}
defer mysqlDB.Close()
if err := mysqlDB.PingContext(ctx); err != nil {
fail("ping mysql", err)
if cfg.ListActiveVINs {
if err := printActiveFrameVINs(ctx, td, cfg); err != nil {
fail("list active frame vins", err)
}
return
}
schemaWriter := stats.NewWriter(mysqlDB, cfg.Location)
@@ -516,10 +531,11 @@ func addRealtimeLocationFallbackAggregates(ctx context.Context, mysqlDB *sql.DB,
currentRow.TotalKM = previousRow.TotalKM
currentSources[i] = currentRow
}
usesPreviousDayBoundary := hasPrevious && stats.IsUsableDailyMileageBoundary(date, previousRow.TS, currentRow.FirstTS.Location())
agg := aggregateFromDailySource(date, protocol, currentRow, previousRow, hasPrevious)
if cfg.StationaryCarry {
agg.QualityReason = stats.QualityReasonStationaryCarry
} else if hasPrevious {
} else if usesPreviousDayBoundary {
agg.QualityReason = "realtime_location_fallback_historical_baseline"
} else {
agg.QualityReason = "realtime_location_fallback_current_day_first_baseline"
@@ -909,7 +925,7 @@ func aggregateFromDailySource(date string, protocol envelope.Protocol, current d
firstEventTime := current.FirstTS
qualityStatus := stats.QualityOK
qualityReason := stats.QualityReasonCurrentDayFirst
if hasPrevious {
if hasPrevious && stats.IsUsableDailyMileageBoundary(date, previous.TS, current.FirstTS.Location()) {
firstKM = previous.TotalKM
firstEventTime = previous.TS
qualityStatus = stats.QualityOK
@@ -1295,10 +1311,32 @@ func fieldsForStats(protocol envelope.Protocol, vin string, text string) map[str
}
fields[key] = value
}
promoteVehicleRunningModeForStats(protocol, fields, text)
promoteFuelCellWorkModeForStats(protocol, fields, text)
return fields
}
func promoteVehicleRunningModeForStats(protocol envelope.Protocol, fields map[string]any, text string) {
if protocol != envelope.ProtocolGB32960 || fields == nil {
return
}
if _, exists := fields[envelope.FieldVehicleRunningMode]; exists {
return
}
for _, key := range []string{"gb32960.vehicle.running_mode", "running_mode"} {
if value, exists := fields[key]; exists {
fields[envelope.FieldVehicleRunningMode] = value
return
}
}
for _, key := range []string{"gb32960.vehicle.running_mode", "running_mode"} {
if value, ok := extractJSONStringField(text, key); ok {
fields[envelope.FieldVehicleRunningMode] = value
return
}
}
}
func promoteFuelCellWorkModeForStats(protocol envelope.Protocol, fields map[string]any, text string) {
if fields == nil {
return
@@ -1383,6 +1421,7 @@ func loadConfig() (config, error) {
DateFrom: dateFrom,
DateTo: dateTo,
Protocols: protocols,
VINs: parseVINs(env("BACKFILL_VINS", "")),
Method: env("BACKFILL_METHOD", "last_diff"),
Limit: envInt("BACKFILL_LIMIT", 0),
DryRun: envBool("BACKFILL_DRY_RUN", true),
@@ -1393,10 +1432,29 @@ func loadConfig() (config, error) {
GPSFallback: envBool("BACKFILL_GPS_FALLBACK", envBool("BACKFILL_JT808_GPS_FALLBACK", true)),
ProgressEvery: int64(envInt("BACKFILL_PROGRESS_EVERY", 100000)),
BaselineLookback: envInt("BACKFILL_BASELINE_LOOKBACK_DAYS", 7),
ListHydrogenVINs: envBool("BACKFILL_LIST_HYDROGEN_VINS", false),
ListActiveVINs: envBool("BACKFILL_LIST_ACTIVE_VINS", false),
Location: loc,
}, nil
}
func parseVINs(value string) []string {
seen := make(map[string]struct{})
vins := make([]string, 0)
for _, raw := range strings.Split(value, ",") {
vin := strings.ToUpper(strings.TrimSpace(raw))
if vin == "" {
continue
}
if _, ok := seen[vin]; ok {
continue
}
seen[vin] = struct{}{}
vins = append(vins, vin)
}
return vins
}
func resolveBackfillDateRange(now time.Time, loc *time.Location) (string, string) {
if loc == nil {
loc = time.FixedZone("Asia/Shanghai", 8*3600)
@@ -1441,6 +1499,13 @@ func queryRawFrames(ctx context.Context, db *sql.DB, cfg config) (*sql.Rows, err
}
where = append(where, "protocol IN ("+strings.Join(quoted, ",")+")")
}
if len(cfg.VINs) > 0 {
quoted := make([]string, 0, len(cfg.VINs))
for _, vin := range cfg.VINs {
quoted = append(quoted, "'"+quote(vin)+"'")
}
where = append(where, "vin IN ("+strings.Join(quoted, ",")+")")
}
where = append(where, realtimeMileageFramePredicate())
sqlText := fmt.Sprintf(`SELECT protocol, vin, phone, device_id, source_endpoint, event_id, message_id, event_time, received_at, parsed_json, raw_text
FROM %s.raw_frames
@@ -1449,10 +1514,61 @@ WHERE %s
if cfg.Limit > 0 {
sqlText += fmt.Sprintf(" LIMIT %d", cfg.Limit)
}
slog.Info("query raw frames", "date_from", cfg.DateFrom, "date_to", cfg.DateTo, "protocols", cfg.Protocols, "limit", cfg.Limit)
slog.Info("query raw frames", "date_from", cfg.DateFrom, "date_to", cfg.DateTo, "protocols", cfg.Protocols, "vins", cfg.VINs, "limit", cfg.Limit)
return db.QueryContext(ctx, sqlText)
}
func printActiveHydrogenVINs(ctx context.Context, db *sql.DB) error {
rows, err := db.QueryContext(ctx, `SELECT DISTINCT UPPER(TRIM(vin))
FROM vehicle_hydrogen_tank_capacity
WHERE active = 1 AND tank_capacity_l > 0 AND vin IS NOT NULL AND TRIM(vin) <> ''
ORDER BY UPPER(TRIM(vin))`)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var vin string
if err := rows.Scan(&vin); err != nil {
return err
}
fmt.Println(vin)
}
return rows.Err()
}
func printActiveFrameVINs(ctx context.Context, db *sql.DB, cfg config) error {
where := backfillTimePredicates(cfg, cfg.DateFrom, nextDate(cfg.DateTo))
where = append(where,
"parse_status = 'OK'",
"vin IS NOT NULL",
"vin <> ''",
)
if len(cfg.Protocols) > 0 {
quoted := make([]string, 0, len(cfg.Protocols))
for _, protocol := range cfg.Protocols {
quoted = append(quoted, "'"+quote(string(protocol))+"'")
}
where = append(where, "protocol IN ("+strings.Join(quoted, ",")+")")
}
where = append(where, realtimeMileageFramePredicate())
rows, err := db.QueryContext(ctx, fmt.Sprintf(`SELECT DISTINCT vin
FROM %s.raw_frames
WHERE %s`, ident(cfg.TDengineDatabase), strings.Join(where, " AND ")))
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var vin string
if err := rows.Scan(&vin); err != nil {
return err
}
fmt.Println(strings.ToUpper(strings.TrimSpace(vin)))
}
return rows.Err()
}
func backfillTimePredicates(cfg config, eventDateFrom string, eventDateToExclusive string) []string {
where := make([]string, 0, 4)
if !cfg.EventTimeFullScan {