perf(api): make batch vehicle search exact

This commit is contained in:
lingniu
2026-07-16 08:58:32 +08:00
parent 251b53125d
commit b78cf1696f
4 changed files with 68 additions and 8 deletions

View File

@@ -911,6 +911,27 @@ func TestHandlerVehicleRealtimeAcceptsBatchKeywords(t *testing.T) {
}
}
func TestHandlerVehicleRealtimeBatchKeywordsAreExact(t *testing.T) {
handler := NewHandler(NewService(NewMockStore()))
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/realtime/vehicles?keywords=%E7%B2%A4AG1831&limit=10", nil)
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
var body struct {
Data struct {
Items []VehicleRealtimeRow `json:"items"`
} `json:"data"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("response JSON should decode: %v body=%s", err, rec.Body.String())
}
if len(body.Data.Items) != 0 {
t.Fatalf("copied batch plates must not fuzzy-match similar identities, got %+v body=%s", body.Data.Items, rec.Body.String())
}
}
func TestHandlerHistoryMileageQualityOps(t *testing.T) {
cases := []struct {
path string

View File

@@ -486,8 +486,17 @@ func buildArchiveMissingFieldStats(missingPlate, missingPhone, missingOEM int) [
func (m *MockStore) VehicleRealtime(_ context.Context, query url.Values) (Page[VehicleRealtimeRow], error) {
rows := m.locations
if keywords := vehicleSearchKeywords(query); len(keywords) > 0 {
batch := strings.TrimSpace(query.Get("keywords")) != ""
rows = keep(rows, func(row RealtimeLocationRow) bool {
vehicle := m.vehicleByVIN(row.VIN)
if batch {
for _, keyword := range keywords {
if strings.EqualFold(row.VIN, keyword) || strings.EqualFold(row.Plate, keyword) || strings.EqualFold(vehicle.Plate, keyword) {
return true
}
}
return false
}
haystack := strings.ToLower(row.VIN + row.Plate + vehicle.Plate + vehicle.Phone + vehicle.OEM)
for _, keyword := range keywords {
if strings.Contains(haystack, strings.ToLower(keyword)) {

View File

@@ -329,13 +329,20 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
args = append(args, protocol)
}
if keywords := vehicleSearchKeywords(query); len(keywords) > 0 {
matches := make([]string, 0, len(keywords))
for _, keyword := range keywords {
matches = append(matches, "(l.vin LIKE ? OR l.plate LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)")
if strings.TrimSpace(query.Get("keywords")) != "" {
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(keywords)), ",")
where = append(where, "(l.vin IN ("+placeholders+") OR l.plate IN ("+placeholders+") OR b.plate IN ("+placeholders+"))")
for range 3 {
for _, keyword := range keywords {
args = append(args, keyword)
}
}
} else {
keyword := keywords[0]
like := "%" + keyword + "%"
where = append(where, "(l.vin LIKE ? OR l.plate LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)")
args = append(args, like, like, like, like, like)
}
where = append(where, "("+strings.Join(matches, " OR ")+")")
}
switch strings.TrimSpace(query.Get("online")) {
case "online":

View File

@@ -3,6 +3,7 @@ package platform
import (
"fmt"
"net/url"
"slices"
"strings"
"testing"
"time"
@@ -217,17 +218,39 @@ func TestBuildVehicleRealtimeSQL(t *testing.T) {
func TestBuildVehicleRealtimeSQLFiltersMultipleVehicleKeywords(t *testing.T) {
built := buildVehicleRealtimeSQL(url.Values{"keywords": {"粤AG18312, 川AHTWO1, 粤AG18312"}, "limit": {"10"}})
if !strings.Contains(built.Text, ") OR (l.vin LIKE ?") || !strings.Contains(built.CountText, ") OR (l.vin LIKE ?") {
t.Fatalf("batch vehicle search must match any keyword in rows and count: %s / %s", built.Text, built.CountText)
for _, text := range []string{built.Text, built.CountText} {
if !strings.Contains(text, "l.vin IN (?,?)") || !strings.Contains(text, "l.plate IN (?,?)") || !strings.Contains(text, "b.plate IN (?,?)") {
t.Fatalf("batch vehicle search must use exact identity sets in rows and count: %s", text)
}
if strings.Contains(text, "LIKE") {
t.Fatalf("batch vehicle search must not expand copied plates into fuzzy predicates: %s", text)
}
}
if len(built.Args) != 12 || built.Args[0] != "%粤AG18312%" || built.Args[5] != "%川AHTWO1%" || built.Args[10] != 10 {
if len(built.Args) != 8 || built.Args[0] != "粤AG18312" || built.Args[1] != "川AHTWO1" || built.Args[2] != "粤AG18312" || built.Args[6] != 10 {
t.Fatalf("batch args = %#v", built.Args)
}
if len(built.CountArgs) != 10 || built.CountArgs[0] != "%粤AG18312%" || built.CountArgs[5] != "%川AHTWO1%" {
if len(built.CountArgs) != 6 || built.CountArgs[0] != "粤AG18312" || built.CountArgs[1] != "川AHTWO1" || built.CountArgs[4] != "粤AG18312" {
t.Fatalf("batch count args = %#v", built.CountArgs)
}
}
func TestBuildVehicleRealtimeSQLBoundsCopiedPlateSet(t *testing.T) {
keywords := make([]string, 0, vehicleSearchKeywordLimit+10)
for index := 0; index < vehicleSearchKeywordLimit+10; index++ {
keywords = append(keywords, fmt.Sprintf("粤A%05d", index))
}
built := buildVehicleRealtimeSQL(url.Values{"keywords": {strings.Join(keywords, ",")}, "limit": {"10"}})
if len(built.Args) != vehicleSearchKeywordLimit*3+2 {
t.Fatalf("bounded batch args = %d, want %d", len(built.Args), vehicleSearchKeywordLimit*3+2)
}
if len(built.CountArgs) != vehicleSearchKeywordLimit*3 {
t.Fatalf("bounded batch count args = %d, want %d", len(built.CountArgs), vehicleSearchKeywordLimit*3)
}
if slices.Contains(built.Args, "粤A00100") {
t.Fatalf("batch query must ignore identities beyond the %d item boundary", vehicleSearchKeywordLimit)
}
}
func TestBuildVehicleRealtimeSQLFiltersServiceStatus(t *testing.T) {
query := url.Values{"serviceStatus": {"degraded"}, "limit": {"8"}}
built := buildVehicleRealtimeSQL(query)