feat(platform): include plates in raw history
This commit is contained in:
@@ -110,7 +110,7 @@ func TestHandlerHistoryMileageQualityOps(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{"/api/history/locations?limit=10", "totalMileageKm"},
|
{"/api/history/locations?limit=10", "totalMileageKm"},
|
||||||
{"/api/history/raw-frames?protocol=GB32960&vin=LB9A32A24R0LS1426&limit=1&includeFields=true", "rawSizeBytes"},
|
{"/api/history/raw-frames?protocol=GB32960&vin=LB9A32A24R0LS1426&limit=1&includeFields=true", "plate"},
|
||||||
{"/api/mileage/daily?limit=10", "dailyMileageKm"},
|
{"/api/mileage/daily?limit=10", "dailyMileageKm"},
|
||||||
{"/api/quality/issues?limit=10", "issueType"},
|
{"/api/quality/issues?limit=10", "issueType"},
|
||||||
{"/api/ops/health", "linkHealth"},
|
{"/api/ops/health", "linkHealth"},
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ func (m *MockStore) RawFrames(_ context.Context, query RawFrameQuery) (Page[RawF
|
|||||||
protocol := defaultString(query.Protocol, "GB32960")
|
protocol := defaultString(query.Protocol, "GB32960")
|
||||||
vin := defaultString(query.VIN, "LB9A32A24R0LS1426")
|
vin := defaultString(query.VIN, "LB9A32A24R0LS1426")
|
||||||
rows := []RawFrameRow{
|
rows := []RawFrameRow{
|
||||||
{ID: "raw-20260703-001", VIN: vin, Protocol: protocol, FrameType: "realtime", DeviceTime: "2026-07-03 20:12:06", ServerTime: "2026-07-03 20:12:06", RawSizeBytes: 430, ParsedFields: fields},
|
{ID: "raw-20260703-001", VIN: vin, Plate: "粤AG18312", Protocol: protocol, FrameType: "realtime", DeviceTime: "2026-07-03 20:12:06", ServerTime: "2026-07-03 20:12:06", RawSizeBytes: 430, ParsedFields: fields},
|
||||||
}
|
}
|
||||||
limit := query.Limit
|
limit := query.Limit
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ type HistoryLocationRow struct {
|
|||||||
type RawFrameRow struct {
|
type RawFrameRow struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
VIN string `json:"vin"`
|
VIN string `json:"vin"`
|
||||||
|
Plate string `json:"plate"`
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
FrameType string `json:"frameType"`
|
FrameType string `json:"frameType"`
|
||||||
DeviceTime string `json:"deviceTime"`
|
DeviceTime string `json:"deviceTime"`
|
||||||
|
|||||||
@@ -305,31 +305,10 @@ func (s *ProductionStore) enrichHistoryLocationPlates(ctx context.Context, items
|
|||||||
seen[vin] = struct{}{}
|
seen[vin] = struct{}{}
|
||||||
vins = append(vins, vin)
|
vins = append(vins, vin)
|
||||||
}
|
}
|
||||||
if len(vins) == 0 {
|
plates, err := s.platesByVIN(ctx, vins)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
placeholders := make([]string, len(vins))
|
|
||||||
args := make([]any, len(vins))
|
|
||||||
for index, vin := range vins {
|
|
||||||
placeholders[index] = "?"
|
|
||||||
args[index] = vin
|
|
||||||
}
|
|
||||||
rows, err := s.db.QueryContext(ctx, `SELECT vin, COALESCE(plate, '') FROM vehicle_identity_binding WHERE vin IN (`+strings.Join(placeholders, ",")+`)`, args...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
plates := map[string]string{}
|
|
||||||
for rows.Next() {
|
|
||||||
var vin, plate string
|
|
||||||
if err := rows.Scan(&vin, &plate); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
plates[vin] = plate
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for index := range items {
|
for index := range items {
|
||||||
if strings.TrimSpace(items[index].Plate) != "" {
|
if strings.TrimSpace(items[index].Plate) != "" {
|
||||||
continue
|
continue
|
||||||
@@ -339,6 +318,56 @@ func (s *ProductionStore) enrichHistoryLocationPlates(ctx context.Context, items
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ProductionStore) enrichRawFramePlates(ctx context.Context, items []RawFrameRow) error {
|
||||||
|
vins := make([]string, 0)
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
for _, item := range items {
|
||||||
|
vin := strings.TrimSpace(item.VIN)
|
||||||
|
if vin == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[vin]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[vin] = struct{}{}
|
||||||
|
vins = append(vins, vin)
|
||||||
|
}
|
||||||
|
plates, err := s.platesByVIN(ctx, vins)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for index := range items {
|
||||||
|
items[index].Plate = plates[items[index].VIN]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductionStore) platesByVIN(ctx context.Context, vins []string) (map[string]string, error) {
|
||||||
|
if len(vins) == 0 {
|
||||||
|
return map[string]string{}, nil
|
||||||
|
}
|
||||||
|
placeholders := make([]string, len(vins))
|
||||||
|
args := make([]any, len(vins))
|
||||||
|
for index, vin := range vins {
|
||||||
|
placeholders[index] = "?"
|
||||||
|
args[index] = vin
|
||||||
|
}
|
||||||
|
rows, err := s.db.QueryContext(ctx, `SELECT vin, COALESCE(plate, '') FROM vehicle_identity_binding WHERE vin IN (`+strings.Join(placeholders, ",")+`)`, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
plates := map[string]string{}
|
||||||
|
for rows.Next() {
|
||||||
|
var vin, plate string
|
||||||
|
if err := rows.Scan(&vin, &plate); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
plates[vin] = plate
|
||||||
|
}
|
||||||
|
return plates, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ProductionStore) RawFrames(ctx context.Context, query RawFrameQuery) (Page[RawFrameRow], error) {
|
func (s *ProductionStore) RawFrames(ctx context.Context, query RawFrameQuery) (Page[RawFrameRow], error) {
|
||||||
if s.tdengine == nil {
|
if s.tdengine == nil {
|
||||||
return Page[RawFrameRow]{Items: []RawFrameRow{}, Total: 0, Limit: query.Limit, Offset: query.Offset}, nil
|
return Page[RawFrameRow]{Items: []RawFrameRow{}, Total: 0, Limit: query.Limit, Offset: query.Offset}, nil
|
||||||
@@ -382,6 +411,9 @@ func (s *ProductionStore) RawFrames(ctx context.Context, query RawFrameQuery) (P
|
|||||||
if built.CountText == "" {
|
if built.CountText == "" {
|
||||||
total = len(items)
|
total = len(items)
|
||||||
}
|
}
|
||||||
|
if err := s.enrichRawFramePlates(ctx, items); err != nil {
|
||||||
|
return Page[RawFrameRow]{}, err
|
||||||
|
}
|
||||||
return Page[RawFrameRow]{Items: items, Total: total, Limit: query.Limit, Offset: query.Offset}, nil
|
return Page[RawFrameRow]{Items: items, Total: total, Limit: query.Limit, Offset: query.Offset}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export interface HistoryLocationRow extends RealtimeLocationRow {
|
|||||||
export interface RawFrameRow {
|
export interface RawFrameRow {
|
||||||
id: string;
|
id: string;
|
||||||
vin: string;
|
vin: string;
|
||||||
|
plate: string;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
frameType: string;
|
frameType: string;
|
||||||
deviceTime: string;
|
deviceTime: string;
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export function History() {
|
|||||||
columns={[
|
columns={[
|
||||||
{ title: 'ID', dataIndex: 'id', width: 260 },
|
{ title: 'ID', dataIndex: 'id', width: 260 },
|
||||||
{ title: 'VIN', dataIndex: 'vin', width: 190 },
|
{ title: 'VIN', dataIndex: 'vin', width: 190 },
|
||||||
|
{ title: '车牌', dataIndex: 'plate', width: 120 },
|
||||||
{ title: '协议', dataIndex: 'protocol', width: 120 },
|
{ title: '协议', dataIndex: 'protocol', width: 120 },
|
||||||
{ title: '帧类型', dataIndex: 'frameType', width: 190 },
|
{ title: '帧类型', dataIndex: 'frameType', width: 190 },
|
||||||
{ title: '大小 B', dataIndex: 'rawSizeBytes', width: 100 },
|
{ title: '大小 B', dataIndex: 'rawSizeBytes', width: 100 },
|
||||||
@@ -177,7 +178,7 @@ export function History() {
|
|||||||
<SideSheet title="RAW 解析字段" visible={Boolean(selectedRaw)} onCancel={() => setSelectedRaw(null)} width={720}>
|
<SideSheet title="RAW 解析字段" visible={Boolean(selectedRaw)} onCancel={() => setSelectedRaw(null)} width={720}>
|
||||||
<Space vertical align="start" spacing={12} style={{ width: '100%' }}>
|
<Space vertical align="start" spacing={12} style={{ width: '100%' }}>
|
||||||
<Typography.Text type="tertiary">
|
<Typography.Text type="tertiary">
|
||||||
{selectedRaw?.protocol ?? '-'} / {selectedRaw?.vin ?? '-'} / {rawFieldCount} 个字段
|
{selectedRaw?.protocol ?? '-'} / {selectedRaw?.plate || selectedRaw?.vin || '-'} / {rawFieldCount} 个字段
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{rawFieldCount === 0 ? (
|
{rawFieldCount === 0 ? (
|
||||||
<Typography.Text type="secondary">当前查询未返回解析字段,请勾选“返回解析字段”或配置字段裁剪后重新查询。</Typography.Text>
|
<Typography.Text type="secondary">当前查询未返回解析字段,请勾选“返回解析字段”或配置字段裁剪后重新查询。</Typography.Text>
|
||||||
|
|||||||
Reference in New Issue
Block a user