feat(platform): surface vehicle quality issues
This commit is contained in:
@@ -56,7 +56,7 @@ func TestHandlerVehicleDetail(t *testing.T) {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
for _, want := range []string{"identity", "realtime", "history", "raw", "mileage", "sources", "sourceStatus"} {
|
||||
for _, want := range []string{"identity", "realtime", "history", "raw", "mileage", "quality", "sources", "sourceStatus", "VIN_MISSING"} {
|
||||
if !strings.Contains(rec.Body.String(), want) {
|
||||
t.Fatalf("response missing %q: %s", want, rec.Body.String())
|
||||
}
|
||||
|
||||
@@ -163,9 +163,15 @@ func (m *MockStore) DailyMileage(_ context.Context, query url.Values) (Page[Dail
|
||||
|
||||
func (m *MockStore) QualityIssues(_ context.Context, query url.Values) (Page[QualityIssueRow], error) {
|
||||
rows := []QualityIssueRow{
|
||||
{VIN: "LB9A32A24P0LS1230", Plate: "粤AFF7936", Protocol: "JT808", IssueType: "VIN_MISSING", Severity: "warning", LastSeen: "2026-07-03 19:58:00", Detail: "phone 未命中 binding 表"},
|
||||
{VIN: "LB9A32A24R0LS1426", Plate: "粤AG18312", Protocol: "JT808", IssueType: "VIN_MISSING", Severity: "warning", LastSeen: "2026-07-03 19:58:00", Detail: "phone 曾未命中 binding 表,已通过手机号关联 VIN"},
|
||||
{VIN: "LNXNEGRR7SR318212", Plate: "川AHTWO1", Protocol: "GB32960", IssueType: "LINK_GAP", Severity: "error", LastSeen: "2026-07-03 18:20:00", Detail: "Hyundai 平台近 60 分钟无转发"},
|
||||
}
|
||||
if vin := strings.TrimSpace(query.Get("vin")); vin != "" {
|
||||
rows = keep(rows, func(row QualityIssueRow) bool { return row.VIN == vin })
|
||||
}
|
||||
if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" {
|
||||
rows = keep(rows, func(row QualityIssueRow) bool { return row.Protocol == protocol })
|
||||
}
|
||||
return page(rows, query), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ type VehicleDetail struct {
|
||||
History Page[HistoryLocationRow] `json:"history"`
|
||||
Raw Page[RawFrameRow] `json:"raw"`
|
||||
Mileage Page[DailyMileageRow] `json:"mileage"`
|
||||
Quality Page[QualityIssueRow] `json:"quality"`
|
||||
}
|
||||
|
||||
type VehicleSourceStatus struct {
|
||||
|
||||
@@ -273,19 +273,38 @@ func (s *ProductionStore) DailyMileage(ctx context.Context, query url.Values) (P
|
||||
|
||||
func (s *ProductionStore) QualityIssues(ctx context.Context, query url.Values) (Page[QualityIssueRow], error) {
|
||||
limit, offset := buildLimitOffset(query)
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT phone, COALESCE(source_endpoint, ''), COALESCE(DATE_FORMAT(latest_seen_at, '%Y-%m-%d %H:%i:%s'), '') FROM jt808_registration WHERE vin = '' OR vin = 'unknown' OR vin IS NULL ORDER BY latest_seen_at DESC LIMIT ? OFFSET ?`, limit, offset)
|
||||
where := []string{"(r.vin = '' OR r.vin = 'unknown' OR r.vin IS NULL)"}
|
||||
args := []any{}
|
||||
if vin := strings.TrimSpace(query.Get("vin")); vin != "" {
|
||||
where = append(where, "(r.vin = ? OR b.vin = ?)")
|
||||
args = append(args, vin, vin)
|
||||
}
|
||||
if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" {
|
||||
where = append(where, "? = 'JT808'")
|
||||
args = append(args, protocol)
|
||||
}
|
||||
if keyword := strings.TrimSpace(query.Get("keyword")); keyword != "" {
|
||||
where = append(where, "(r.phone LIKE ? OR COALESCE(r.source_endpoint, '') LIKE ? OR COALESCE(b.plate, '') LIKE ?)")
|
||||
like := "%" + keyword + "%"
|
||||
args = append(args, like, like, like)
|
||||
}
|
||||
args = append(args, limit, offset)
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT COALESCE(b.vin, 'unknown') AS vin, COALESCE(b.plate, '') AS plate, r.phone, COALESCE(r.source_endpoint, ''), COALESCE(DATE_FORMAT(r.latest_seen_at, '%Y-%m-%d %H:%i:%s'), '') `+
|
||||
`FROM jt808_registration r LEFT JOIN vehicle_identity_binding b ON b.phone = r.phone `+
|
||||
`WHERE `+strings.Join(where, " AND ")+` ORDER BY r.latest_seen_at DESC LIMIT ? OFFSET ?`, args...)
|
||||
if err != nil {
|
||||
return Page[QualityIssueRow]{}, nil
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]QualityIssueRow, 0)
|
||||
for rows.Next() {
|
||||
var phone, source, lastSeen string
|
||||
if err := rows.Scan(&phone, &source, &lastSeen); err != nil {
|
||||
var vin, plate, phone, source, lastSeen string
|
||||
if err := rows.Scan(&vin, &plate, &phone, &source, &lastSeen); err != nil {
|
||||
return Page[QualityIssueRow]{}, err
|
||||
}
|
||||
items = append(items, QualityIssueRow{
|
||||
VIN: "unknown",
|
||||
VIN: vin,
|
||||
Plate: plate,
|
||||
Protocol: "JT808",
|
||||
IssueType: "VIN_MISSING",
|
||||
Severity: "warning",
|
||||
|
||||
@@ -69,10 +69,12 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
|
||||
historyQuery := url.Values{"vin": {resolvedVIN}, "limit": {"20"}}
|
||||
rawQuery := RawFrameQuery{VIN: resolvedVIN, IncludeFields: true, Limit: 10}
|
||||
mileageQuery := url.Values{"vin": {resolvedVIN}, "limit": {"20"}}
|
||||
qualityQuery := url.Values{"vin": {resolvedVIN}, "limit": {"20"}}
|
||||
if protocol != "" {
|
||||
realtimeQuery.Set("protocol", protocol)
|
||||
historyQuery.Set("protocol", protocol)
|
||||
rawQuery.Protocol = protocol
|
||||
qualityQuery.Set("protocol", protocol)
|
||||
}
|
||||
realtime, err := s.store.RealtimeLocations(ctx, realtimeQuery)
|
||||
if err != nil {
|
||||
@@ -90,6 +92,10 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
|
||||
if err != nil {
|
||||
return VehicleDetail{}, err
|
||||
}
|
||||
quality, err := s.store.QualityIssues(ctx, qualityQuery)
|
||||
if err != nil {
|
||||
return VehicleDetail{}, err
|
||||
}
|
||||
sourceStatus := vehicleSourceStatus(vehicles.Items, realtime.Items, history.Items, raw.Items, mileage.Items)
|
||||
sourceStatus = s.enrichVehicleSourceStatus(ctx, resolvedVIN, sourceStatus)
|
||||
return VehicleDetail{
|
||||
@@ -101,6 +107,7 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
|
||||
History: history,
|
||||
Raw: raw,
|
||||
Mileage: mileage,
|
||||
Quality: quality,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user