116 lines
2.8 KiB
Go
116 lines
2.8 KiB
Go
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
|
|
}
|