package identity import ( "context" "database/sql" "errors" "strings" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) type Resolver interface { Resolve(context.Context, envelope.FrameEnvelope) (envelope.FrameEnvelope, error) } type NoopResolver struct{} func (NoopResolver) Resolve(_ context.Context, env envelope.FrameEnvelope) (envelope.FrameEnvelope, error) { return env, nil } type MySQLResolver struct { db *sql.DB table string } func NewMySQLResolver(db *sql.DB, table string) *MySQLResolver { if db == nil { panic("identity db must not be nil") } table = strings.TrimSpace(table) if table == "" || !safeIdentifier(table) { table = "vehicle_identity_binding" } return &MySQLResolver{db: db, table: table} } func safeIdentifier(value string) bool { if value == "" { return false } for _, r := range value { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { continue } return false } return true } func (r *MySQLResolver) Resolve(ctx context.Context, env envelope.FrameEnvelope) (envelope.FrameEnvelope, error) { if strings.TrimSpace(env.VIN) != "" { return env, nil } candidates := CandidateKeys(env) for _, candidate := range candidates { vin, err := r.lookup(ctx, candidate.Column, candidate.Value) if err != nil { if errors.Is(err, sql.ErrNoRows) { continue } return env, err } if strings.TrimSpace(vin) != "" { env.VIN = strings.TrimSpace(vin) if env.Parsed == nil { env.Parsed = map[string]any{} } env.Parsed["identity"] = map[string]any{ "resolved": true, "source": candidate.Column, "value": candidate.Value, } env.EventID = env.StableEventID() return env, nil } } return env, nil } func (r *MySQLResolver) lookup(ctx context.Context, column string, value string) (string, error) { query := "SELECT vin FROM " + r.table + " WHERE " + column + " = ? AND vin IS NOT NULL AND vin <> '' ORDER BY updated_at DESC LIMIT 1" var vin string err := r.db.QueryRowContext(ctx, query, value).Scan(&vin) return vin, err } type CandidateKey struct { Column string Value string } func CandidateKeys(env envelope.FrameEnvelope) []CandidateKey { var out []CandidateKey add := func(column string, value string) { value = strings.TrimSpace(value) if value == "" || strings.EqualFold(value, "unknown") { return } for _, existing := range out { if existing.Column == column && existing.Value == value { return } } out = append(out, CandidateKey{Column: column, Value: value}) } add("phone", env.Phone) if trimmed := strings.TrimLeft(strings.TrimSpace(env.Phone), "0"); trimmed != "" { add("phone", trimmed) } add("device_id", env.DeviceID) add("plate", env.Plate) add("vin", env.VehicleKeyHint) return out }