fix(platform): preserve fuzzy vehicle overview search
This commit is contained in:
@@ -233,46 +233,14 @@ func (s *ProductionStore) VehicleServiceOverviews(ctx context.Context, query Veh
|
||||
if len(keywords) == 0 {
|
||||
return Page[VehicleServiceOverview]{Items: []VehicleServiceOverview{}, Total: 0, Limit: query.Limit, Offset: query.Offset}, nil
|
||||
}
|
||||
placeholders := repeatPlaceholders(len(keywords))
|
||||
protocolJoin := ""
|
||||
args := make([]any, 0, len(keywords)*5+1)
|
||||
for i := 0; i < 3; i++ {
|
||||
for _, keyword := range keywords {
|
||||
args = append(args, keyword)
|
||||
}
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
for _, keyword := range keywords {
|
||||
args = append(args, keyword)
|
||||
}
|
||||
}
|
||||
if protocol := strings.TrimSpace(query.Protocol); protocol != "" {
|
||||
protocolJoin = " AND s.protocol = ? "
|
||||
args = append(args, protocol)
|
||||
}
|
||||
sqlText := `SELECT i.vin, COALESCE(MAX(NULLIF(i.plate, '')), '') AS plate, ` +
|
||||
`COALESCE(MAX(NULLIF(i.phone, '')), '') AS phone, COALESCE(MAX(NULLIF(i.oem, '')), '') AS oem, ` +
|
||||
`COALESCE(GROUP_CONCAT(DISTINCT s.protocol ORDER BY s.protocol SEPARATOR ','), '') AS protocols, ` +
|
||||
`COUNT(DISTINCT s.protocol) AS source_count, ` +
|
||||
`COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) AS online_source_count, ` +
|
||||
`COALESCE(DATE_FORMAT(MAX(s.updated_at), '%Y-%m-%d %H:%i:%s'), '') AS last_seen, ` +
|
||||
`COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(s.protocol ORDER BY s.updated_at DESC, s.protocol ASC), ',', 1), '') AS primary_protocol ` +
|
||||
`FROM (` +
|
||||
`SELECT b.vin, b.plate, b.phone, b.oem FROM vehicle_identity_binding b ` +
|
||||
`WHERE b.vin IN (` + placeholders + `) OR b.plate IN (` + placeholders + `) OR b.phone IN (` + placeholders + `) ` +
|
||||
`UNION ` +
|
||||
`SELECT s.vin, COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), b.plate, '') AS plate, COALESCE(b.phone, '') AS phone, COALESCE(b.oem, '') AS oem ` +
|
||||
`FROM vehicle_realtime_snapshot s LEFT JOIN vehicle_identity_binding b ON b.vin = s.vin ` +
|
||||
`WHERE s.vin IN (` + placeholders + `) OR s.plate IN (` + placeholders + `) ` +
|
||||
`GROUP BY s.vin, b.plate, b.phone, b.oem` +
|
||||
`) i LEFT JOIN vehicle_realtime_snapshot s ON s.vin = i.vin ` + protocolJoin +
|
||||
`WHERE i.vin IS NOT NULL AND i.vin <> '' GROUP BY i.vin`
|
||||
rows, err := s.db.QueryContext(ctx, sqlText, args...)
|
||||
built := buildVehicleServiceOverviewBatchSQL(query)
|
||||
rows, err := s.db.QueryContext(ctx, built.Text, built.Args...)
|
||||
if err != nil {
|
||||
return Page[VehicleServiceOverview]{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
byKey := map[string]*VehicleServiceOverview{}
|
||||
overviews := make([]VehicleServiceOverview, 0, len(keywords))
|
||||
for rows.Next() {
|
||||
var overview VehicleServiceOverview
|
||||
var protocols string
|
||||
@@ -286,9 +254,11 @@ func (s *ProductionStore) VehicleServiceOverviews(ctx context.Context, query Veh
|
||||
overview.RealtimeCount = overview.SourceCount
|
||||
overview.CoverageStatus = coverageStatus(overview.SourceCount, overview.OnlineSourceCount)
|
||||
overview.ServiceStatus = buildVehicleServiceStatus(strings.TrimSpace(overview.VIN) != "", statusesForOverview(&overview))
|
||||
overviews = append(overviews, overview)
|
||||
current := &overviews[len(overviews)-1]
|
||||
for _, key := range []string{overview.VIN, overview.Plate, overview.Phone} {
|
||||
if trimmed := strings.ToLower(strings.TrimSpace(key)); trimmed != "" {
|
||||
byKey[trimmed] = &overview
|
||||
byKey[trimmed] = current
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,12 +271,81 @@ func (s *ProductionStore) VehicleServiceOverviews(ctx context.Context, query Veh
|
||||
items = append(items, *overview)
|
||||
continue
|
||||
}
|
||||
matched := false
|
||||
for index := range overviews {
|
||||
if vehicleServiceOverviewMatchesKeyword(overviews[index], keyword) {
|
||||
items = append(items, overviews[index])
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
continue
|
||||
}
|
||||
resolution := VehicleIdentityResolution{LookupKey: keyword, Protocols: []string{}}
|
||||
items = append(items, *buildVehicleServiceOverview("", keyword, &resolution, nil, nil, nil, Page[HistoryLocationRow]{}, Page[RawFrameRow]{}, Page[DailyMileageRow]{}, Page[QualityIssueRow]{}))
|
||||
}
|
||||
return Page[VehicleServiceOverview]{Items: items, Total: len(items), Limit: query.Limit, Offset: query.Offset}, nil
|
||||
}
|
||||
|
||||
func buildVehicleServiceOverviewBatchSQL(query VehicleOverviewBatchQuery) SQLQuery {
|
||||
keywords := normalizedKeywords(query.Keywords)
|
||||
identityWhere, args := fuzzyKeywordWhere("b", []string{"vin", "plate", "phone", "oem"}, keywords)
|
||||
realtimeWhere, realtimeArgs := fuzzyKeywordWhere("s", []string{"vin", "plate"}, keywords)
|
||||
args = append(args, realtimeArgs...)
|
||||
protocolJoin := ""
|
||||
if protocol := strings.TrimSpace(query.Protocol); protocol != "" {
|
||||
protocolJoin = " AND s.protocol = ? "
|
||||
args = append(args, protocol)
|
||||
}
|
||||
return SQLQuery{
|
||||
Text: `SELECT i.vin, COALESCE(MAX(NULLIF(i.plate, '')), '') AS plate, ` +
|
||||
`COALESCE(MAX(NULLIF(i.phone, '')), '') AS phone, COALESCE(MAX(NULLIF(i.oem, '')), '') AS oem, ` +
|
||||
`COALESCE(GROUP_CONCAT(DISTINCT s.protocol ORDER BY s.protocol SEPARATOR ','), '') AS protocols, ` +
|
||||
`COUNT(DISTINCT s.protocol) AS source_count, ` +
|
||||
`COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) AS online_source_count, ` +
|
||||
`COALESCE(DATE_FORMAT(MAX(s.updated_at), '%Y-%m-%d %H:%i:%s'), '') AS last_seen, ` +
|
||||
`COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(s.protocol ORDER BY s.updated_at DESC, s.protocol ASC), ',', 1), '') AS primary_protocol ` +
|
||||
`FROM (` +
|
||||
`SELECT b.vin, b.plate, b.phone, b.oem FROM vehicle_identity_binding b WHERE ` + identityWhere + ` ` +
|
||||
`UNION ` +
|
||||
`SELECT s.vin, COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), b.plate, '') AS plate, COALESCE(b.phone, '') AS phone, COALESCE(b.oem, '') AS oem ` +
|
||||
`FROM vehicle_realtime_snapshot s LEFT JOIN vehicle_identity_binding b ON b.vin = s.vin ` +
|
||||
`WHERE ` + realtimeWhere + ` ` +
|
||||
`GROUP BY s.vin, b.plate, b.phone, b.oem` +
|
||||
`) i LEFT JOIN vehicle_realtime_snapshot s ON s.vin = i.vin ` + protocolJoin +
|
||||
`WHERE i.vin IS NOT NULL AND i.vin <> '' GROUP BY i.vin`,
|
||||
Args: args,
|
||||
}
|
||||
}
|
||||
|
||||
func fuzzyKeywordWhere(alias string, fields []string, keywords []string) (string, []any) {
|
||||
clauses := make([]string, 0, len(keywords))
|
||||
args := make([]any, 0, len(keywords)*len(fields))
|
||||
for _, keyword := range keywords {
|
||||
fieldClauses := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
fieldClauses = append(fieldClauses, alias+"."+field+" LIKE ?")
|
||||
args = append(args, "%"+keyword+"%")
|
||||
}
|
||||
clauses = append(clauses, "("+strings.Join(fieldClauses, " OR ")+")")
|
||||
}
|
||||
return strings.Join(clauses, " OR "), args
|
||||
}
|
||||
|
||||
func vehicleServiceOverviewMatchesKeyword(overview VehicleServiceOverview, keyword string) bool {
|
||||
keyword = strings.ToLower(strings.TrimSpace(keyword))
|
||||
if keyword == "" {
|
||||
return false
|
||||
}
|
||||
for _, value := range []string{overview.VIN, overview.Plate, overview.Phone, overview.OEM} {
|
||||
if strings.Contains(strings.ToLower(strings.TrimSpace(value)), keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *ProductionStore) RealtimeLocations(ctx context.Context, query url.Values) (Page[RealtimeLocationRow], error) {
|
||||
built := buildRealtimeLocationSQL(query)
|
||||
total := 0
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildVehicleServiceOverviewBatchSQLUsesFuzzyKeywordMatching(t *testing.T) {
|
||||
built := buildVehicleServiceOverviewBatchSQL(VehicleOverviewBatchQuery{
|
||||
Keywords: []string{"AG183", "R0LS1426"},
|
||||
Protocol: "JT808",
|
||||
})
|
||||
if !strings.Contains(built.Text, "LIKE ?") {
|
||||
t.Fatalf("batch overview SQL should support fuzzy keyword matching, got %s", built.Text)
|
||||
}
|
||||
if !strings.Contains(built.Text, "b.oem LIKE ?") {
|
||||
t.Fatalf("batch overview SQL should include OEM in vehicle identity search, got %s", built.Text)
|
||||
}
|
||||
if len(built.Args) == 0 || built.Args[0] != "%AG183%" {
|
||||
t.Fatalf("batch overview SQL should bind fuzzy keyword args first, got %#v", built.Args)
|
||||
}
|
||||
if got := built.Args[len(built.Args)-1]; got != "JT808" {
|
||||
t.Fatalf("protocol should remain an exact filter, got %#v args=%#v", got, built.Args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVehicleServiceOverviewMatchesPartialKeyword(t *testing.T) {
|
||||
overview := VehicleServiceOverview{
|
||||
VIN: "LB9A32A24R0LS1426",
|
||||
Plate: "粤AG18312",
|
||||
Phone: "13307795425",
|
||||
OEM: "G7s",
|
||||
}
|
||||
for _, keyword := range []string{"R0LS1426", "AG183", "077954", "g7"} {
|
||||
if !vehicleServiceOverviewMatchesKeyword(overview, keyword) {
|
||||
t.Fatalf("overview %+v should match partial keyword %q", overview, keyword)
|
||||
}
|
||||
}
|
||||
if vehicleServiceOverviewMatchesKeyword(overview, "不存在") {
|
||||
t.Fatalf("overview %+v should not match unrelated keyword", overview)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user