diff --git a/docs/architecture/storage-minimal-contract.md b/docs/architecture/storage-minimal-contract.md index 9cd1abe9..04047b59 100644 --- a/docs/architecture/storage-minimal-contract.md +++ b/docs/architecture/storage-minimal-contract.md @@ -63,7 +63,7 @@ - 状态:schema 使用 `PRIMARY KEY (protocol, vin)`,不保留自增 `id` 和 `created_at`,对外只暴露最新 `updated_at`。 - 索引:不在 `vehicle_realtime_location` 维护经纬度组合索引;实时表只回答当前态,地理范围和轨迹类查询走 TDengine 历史位置。 - 查询:`/api/realtime/snapshots`、`/api/realtime/locations` 默认不执行 `COUNT(*)`,`total` 表示本页返回条数;只有需要精确总数时传 `includeTotal=true`。 - - 写入:车牌从 `vehicle_identity_binding` 反查时使用 VIN 级短 TTL 内存缓存,避免实时帧每条都打 MySQL;缓存不作为事实来源。 + - 写入:车牌从 `vehicle_identity_binding` 按 VIN 主键直查,并使用 VIN 级短 TTL 内存缓存,避免实时帧每条都打 MySQL;缓存不作为事实来源。 - 生产:项目上线前可直接重建这两张 MySQL 表,实时数据会从 Kafka 新消息继续投影。 7. identity 层只保留两张表。 diff --git a/go/vehicle-gateway/internal/realtime/snapshot_writer.go b/go/vehicle-gateway/internal/realtime/snapshot_writer.go index 4a055182..4d6e931d 100644 --- a/go/vehicle-gateway/internal/realtime/snapshot_writer.go +++ b/go/vehicle-gateway/internal/realtime/snapshot_writer.go @@ -288,7 +288,7 @@ func (r *BindingPlateResolver) PlateByVIN(ctx context.Context, vin string) (stri if vin == "" { return "", sql.ErrNoRows } - query := "SELECT plate FROM " + r.table + " WHERE vin = ? AND plate IS NOT NULL AND plate <> '' ORDER BY updated_at DESC LIMIT 1" + query := "SELECT plate FROM " + r.table + " WHERE vin = ? AND plate IS NOT NULL AND plate <> ''" var plate string err := r.queryer.QueryRowContext(ctx, query, vin).Scan(&plate) if err != nil { diff --git a/go/vehicle-gateway/internal/realtime/snapshot_writer_test.go b/go/vehicle-gateway/internal/realtime/snapshot_writer_test.go index bda4ec82..ff6ee420 100644 --- a/go/vehicle-gateway/internal/realtime/snapshot_writer_test.go +++ b/go/vehicle-gateway/internal/realtime/snapshot_writer_test.go @@ -216,6 +216,28 @@ func TestSnapshotWriterBackfillsPlateFromBindingByVIN(t *testing.T) { } } +func TestBindingPlateResolverUsesPrimaryKeyLookupWithoutSort(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock.New() error = %v", err) + } + defer db.Close() + mock.ExpectQuery("SELECT plate FROM vehicle_identity_binding WHERE vin = \\? AND plate IS NOT NULL AND plate <> ''$"). + WithArgs("VIN001"). + WillReturnRows(sqlmock.NewRows([]string{"plate"}).AddRow("沪A12345")) + + plate, err := NewBindingPlateResolver(db, "vehicle_identity_binding").PlateByVIN(context.Background(), " VIN001 ") + if err != nil { + t.Fatalf("PlateByVIN() error = %v", err) + } + if plate != "沪A12345" { + t.Fatalf("plate = %q", plate) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("sql expectations: %v", err) + } +} + func TestSnapshotWriterCachesBindingPlateByVIN(t *testing.T) { exec := &recordingSnapshotExec{} resolver := &recordingPlateResolver{plate: "沪A12345"}