47 KiB
Vehicle Data Platform Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build and deploy a new vehicle-data-platform project with a Go BFF backend and a React + TypeScript + Semi UI frontend for vehicle data management.
Architecture: The platform is a new project folder under the existing repository. The Go API acts as a BFF that reads MySQL, Redis, TDengine, and local ops endpoints, then serves unified /api/* responses plus the built frontend. The frontend is a Semi UI console with dashboard, vehicles, realtime, vehicle detail, history, mileage, and quality/ops pages.
Tech Stack: Go, React, TypeScript, Vite, Semi UI, ECharts, MySQL, Redis, TDengine, systemd.
File Structure
Create this structure:
vehicle-data-platform/
apps/
api/
go.mod
cmd/platform-api/main.go
internal/app/server.go
internal/app/routes.go
internal/config/config.go
internal/httpx/response.go
internal/httpx/response_test.go
internal/platform/model.go
internal/platform/mock_store.go
internal/platform/service.go
internal/platform/service_test.go
internal/platform/handler.go
internal/platform/handler_test.go
internal/static/static.go
web/
package.json
index.html
tsconfig.json
vite.config.ts
src/main.tsx
src/App.tsx
src/styles/tokens.css
src/styles/global.css
src/api/client.ts
src/api/types.ts
src/layout/AppShell.tsx
src/components/StatusTag.tsx
src/components/PageHeader.tsx
src/components/DataEmpty.tsx
src/pages/Dashboard.tsx
src/pages/Vehicles.tsx
src/pages/Realtime.tsx
src/pages/VehicleDetail.tsx
src/pages/History.tsx
src/pages/Mileage.tsx
src/pages/Quality.tsx
src/test/App.test.tsx
src/test/setup.ts
deploy/
systemd/lingniu-vehicle-platform.service
docs/
product-spec.md
api-contract.md
deployment.md
package.json
Do not modify the existing Go gateway services unless a task explicitly says so.
Task 1: Visual Concept And Frontend Design System
Files:
-
Create:
vehicle-data-platform/docs/product-spec.md -
Create:
vehicle-data-platform/apps/web/src/styles/tokens.css -
Create:
vehicle-data-platform/apps/web/src/styles/global.css -
Step 1: Write the product UI brief
Create vehicle-data-platform/docs/product-spec.md with:
# Vehicle Data Platform Product Spec
## Visual Direction
The UI is a professional vehicle data operations console. It uses Semi UI as the component base, a restrained gray-white control-room palette, dense but readable tables, right-side drawers for detail, and blue-cyan accents for primary actions. It should feel closer to a cloud data console than a marketing dashboard.
## Primary Screens
1. Dashboard: KPI summary, protocol distribution, vehicle map preview, quality issues, and link health.
2. Vehicles: table-first identity and vehicle registry.
3. Realtime: table/map switch for current vehicle state.
4. Vehicle Detail: identity header and tabs for latest state, history, raw, mileage, and quality.
5. History: location and raw-frame query workspace.
6. Mileage: daily and range mileage analysis.
7. Quality: data quality and link health workspace.
## Interaction Rules
- Tables are the default data surface.
- Filters stay above the table.
- Details open in a right drawer unless a full route is needed.
- Error and empty states must be explicit.
- No decorative hero, no oversized marketing card layout, no dark large-screen style.
- Step 2: Create design tokens
Create vehicle-data-platform/apps/web/src/styles/tokens.css:
:root {
--vp-bg: #f5f7fb;
--vp-surface: #ffffff;
--vp-surface-muted: #f9fafc;
--vp-border: #e6e9f0;
--vp-text: #172033;
--vp-text-muted: #667085;
--vp-text-subtle: #98a2b3;
--vp-primary: #1664ff;
--vp-primary-hover: #0f4fd6;
--vp-cyan: #00a4b8;
--vp-success: #12b76a;
--vp-warning: #f79009;
--vp-danger: #f04438;
--vp-radius: 8px;
--vp-shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
--vp-shell-sidebar: 232px;
--vp-shell-header: 56px;
--vp-page-gutter: 24px;
}
- Step 3: Create global styling
Create vehicle-data-platform/apps/web/src/styles/global.css:
@import './tokens.css';
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 1280px;
background: var(--vp-bg);
color: var(--vp-text);
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.vp-page {
padding: var(--vp-page-gutter);
}
.vp-section {
background: var(--vp-surface);
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
box-shadow: var(--vp-shadow-sm);
}
- Step 4: Commit
git add vehicle-data-platform/docs/product-spec.md vehicle-data-platform/apps/web/src/styles/tokens.css vehicle-data-platform/apps/web/src/styles/global.css
git commit -m "docs(platform): define product UI direction"
Task 2: Scaffold Monorepo And Tooling
Files:
-
Create:
vehicle-data-platform/package.json -
Create:
vehicle-data-platform/apps/web/package.json -
Create:
vehicle-data-platform/apps/web/index.html -
Create:
vehicle-data-platform/apps/web/tsconfig.json -
Create:
vehicle-data-platform/apps/web/vite.config.ts -
Create:
vehicle-data-platform/apps/web/src/test/setup.ts -
Create:
vehicle-data-platform/apps/api/go.mod -
Step 1: Create root package file
Create vehicle-data-platform/package.json:
{
"private": true,
"scripts": {
"web:dev": "npm --prefix apps/web run dev",
"web:build": "npm --prefix apps/web run build",
"web:test": "npm --prefix apps/web run test",
"api:test": "cd apps/api && go test ./...",
"build": "npm run web:build && cd apps/api && go build -o ../../dist/platform-api ./cmd/platform-api"
}
}
- Step 2: Create web package
Create vehicle-data-platform/apps/web/package.json:
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 20301",
"build": "tsc -b && vite build",
"test": "vitest run"
},
"dependencies": {
"@douyinfe/semi-icons": "^2.71.0",
"@douyinfe/semi-ui": "^2.71.0",
"@vitejs/plugin-react": "^4.3.4",
"echarts": "^5.6.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"vite": "^6.0.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"typescript": "^5.7.2",
"vitest": "^2.1.8"
}
}
- Step 3: Create Vite entry files
Create vehicle-data-platform/apps/web/index.html:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>车辆数据管理中台</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Create vehicle-data-platform/apps/web/tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ES2020"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
Create vehicle-data-platform/apps/web/vite.config.ts:
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://127.0.0.1:20300'
}
},
test: {
environment: 'jsdom',
setupFiles: './src/test/setup.ts'
}
});
Create vehicle-data-platform/apps/web/src/test/setup.ts:
import '@testing-library/jest-dom/vitest';
- Step 4: Create API module
Create vehicle-data-platform/apps/api/go.mod:
module lingniu-vehicle-platform/api
go 1.23
require (
github.com/go-sql-driver/mysql v1.8.1
github.com/redis/go-redis/v9 v9.7.0
github.com/taosdata/driver-go/v3 v3.6.0
)
- Step 5: Verify scaffolding
Run:
cd vehicle-data-platform/apps/api && go test ./...
cd ../../apps/web && npm install && npm run test
Expected:
-
Go reports no packages or passing packages.
-
Web dependencies install and Vitest reports no tests or passing tests.
-
Step 6: Commit
git add vehicle-data-platform/package.json vehicle-data-platform/apps/web vehicle-data-platform/apps/api/go.mod
git commit -m "chore(platform): scaffold vehicle data platform"
Task 3: Backend Response Envelope And Config
Files:
-
Create:
vehicle-data-platform/apps/api/internal/httpx/response.go -
Create:
vehicle-data-platform/apps/api/internal/httpx/response_test.go -
Create:
vehicle-data-platform/apps/api/internal/config/config.go -
Step 1: Write response tests
Create vehicle-data-platform/apps/api/internal/httpx/response_test.go:
package httpx
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestWriteOKWrapsData(t *testing.T) {
rec := httptest.NewRecorder()
WriteOK(rec, "trace-1", map[string]any{"online": 12})
if rec.Code != http.StatusOK {
t.Fatalf("status = %d", rec.Code)
}
var body map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["traceId"] != "trace-1" {
t.Fatalf("traceId = %#v", body["traceId"])
}
data := body["data"].(map[string]any)
if data["online"].(float64) != 12 {
t.Fatalf("data = %#v", data)
}
}
func TestWriteErrorWrapsError(t *testing.T) {
rec := httptest.NewRecorder()
WriteError(rec, http.StatusBadRequest, "BAD_QUERY", "查询失败", "vin required", "trace-2")
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d", rec.Code)
}
if got := rec.Body.String(); !containsAll(got, []string{"BAD_QUERY", "查询失败", "vin required", "trace-2"}) {
t.Fatalf("body = %s", got)
}
}
func containsAll(value string, parts []string) bool {
for _, part := range parts {
if !strings.Contains(value, part) {
return false
}
}
return true
}
- Step 2: Run response tests and see failure
Run:
cd vehicle-data-platform/apps/api
go test ./internal/httpx
Expected: fail because WriteOK and WriteError are undefined.
- Step 3: Implement response helpers
Create vehicle-data-platform/apps/api/internal/httpx/response.go:
package httpx
import (
"encoding/json"
"net/http"
"time"
)
type Envelope struct {
Data any `json:"data,omitempty"`
Error *Error `json:"error,omitempty"`
TraceID string `json:"traceId"`
Timestamp int64 `json:"timestamp"`
}
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Detail string `json:"detail,omitempty"`
}
func WriteOK(w http.ResponseWriter, traceID string, data any) {
writeJSON(w, http.StatusOK, Envelope{Data: data, TraceID: traceID, Timestamp: time.Now().UnixMilli()})
}
func WriteError(w http.ResponseWriter, status int, code string, message string, detail string, traceID string) {
writeJSON(w, status, Envelope{Error: &Error{Code: code, Message: message, Detail: detail}, TraceID: traceID, Timestamp: time.Now().UnixMilli()})
}
func writeJSON(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value)
}
- Step 4: Implement config
Create vehicle-data-platform/apps/api/internal/config/config.go:
package config
import (
"os"
"strconv"
"strings"
"time"
)
type Config struct {
HTTPAddr string
MySQLDSN string
RedisAddr string
RedisUsername string
RedisPassword string
RedisDB int
TDengineDSN string
TDengineDatabase string
CapacityCheckBin string
AuthToken string
RequestTimeout time.Duration
}
func Load() Config {
return Config{
HTTPAddr: env("HTTP_ADDR", ":20300"),
MySQLDSN: env("MYSQL_DSN", ""),
RedisAddr: env("REDIS_ADDR", "127.0.0.1:6379"),
RedisUsername: env("REDIS_USERNAME", ""),
RedisPassword: env("REDIS_PASSWORD", ""),
RedisDB: envInt("REDIS_DB", 0),
TDengineDSN: env("TDENGINE_DSN", ""),
TDengineDatabase: env("TDENGINE_DATABASE", "lingniu_vehicle_ts"),
CapacityCheckBin: env("CAPACITY_CHECK_BIN", "/opt/lingniu-go-native/current/capacity-check"),
AuthToken: env("AUTH_TOKEN", ""),
RequestTimeout: time.Duration(envInt("REQUEST_TIMEOUT_MS", 5000)) * time.Millisecond,
}
}
func env(key string, fallback string) string {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
return value
}
func envInt(key string, fallback int) int {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := strconv.Atoi(value)
if err != nil {
return fallback
}
return parsed
}
- Step 5: Run tests
Run:
cd vehicle-data-platform/apps/api
go test ./...
Expected: pass.
- Step 6: Commit
git add vehicle-data-platform/apps/api/internal/httpx vehicle-data-platform/apps/api/internal/config
git commit -m "feat(platform-api): add response envelope and config"
Task 4: Backend Platform Service With Mock Store
Files:
-
Create:
vehicle-data-platform/apps/api/internal/platform/model.go -
Create:
vehicle-data-platform/apps/api/internal/platform/mock_store.go -
Create:
vehicle-data-platform/apps/api/internal/platform/service.go -
Create:
vehicle-data-platform/apps/api/internal/platform/service_test.go -
Step 1: Write service tests
Create vehicle-data-platform/apps/api/internal/platform/service_test.go:
package platform
import (
"context"
"testing"
)
func TestServiceDashboardSummary(t *testing.T) {
service := NewService(NewMockStore())
summary, err := service.DashboardSummary(context.Background())
if err != nil {
t.Fatal(err)
}
if summary.OnlineVehicles == 0 {
t.Fatalf("online vehicles should be positive: %#v", summary)
}
if len(summary.Protocols) == 0 {
t.Fatalf("protocols missing: %#v", summary)
}
}
func TestServiceVehicleListPaginates(t *testing.T) {
service := NewService(NewMockStore())
page, err := service.ListVehicles(context.Background(), VehicleQuery{Limit: 1, Offset: 0})
if err != nil {
t.Fatal(err)
}
if len(page.Items) != 1 || page.Total < 1 {
t.Fatalf("page = %#v", page)
}
}
- Step 2: Run service tests and see failure
Run:
cd vehicle-data-platform/apps/api
go test ./internal/platform
Expected: fail because package types are undefined.
- Step 3: Implement models
Create vehicle-data-platform/apps/api/internal/platform/model.go:
package platform
type Page[T any] struct {
Items []T `json:"items"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
type ProtocolStat struct {
Protocol string `json:"protocol"`
Online int64 `json:"online"`
Total int64 `json:"total"`
}
type DashboardSummary struct {
OnlineVehicles int64 `json:"onlineVehicles"`
ActiveToday int64 `json:"activeToday"`
FrameToday int64 `json:"frameToday"`
IssueVehicles int64 `json:"issueVehicles"`
KafkaLag int64 `json:"kafkaLag"`
Protocols []ProtocolStat `json:"protocols"`
LinkHealth []LinkHealth `json:"linkHealth"`
}
type LinkHealth struct {
Name string `json:"name"`
Status string `json:"status"`
Detail string `json:"detail,omitempty"`
}
type VehicleQuery struct {
Keyword string
Protocol string
Online string
Limit int
Offset int
}
type VehicleRow struct {
VIN string `json:"vin"`
Plate string `json:"plate"`
Phone string `json:"phone"`
OEM string `json:"oem"`
Protocol string `json:"protocol"`
Online bool `json:"online"`
LastSeen string `json:"lastSeen"`
LocationText string `json:"locationText"`
BindingScore int `json:"bindingScore"`
}
- Step 4: Implement mock store
Create vehicle-data-platform/apps/api/internal/platform/mock_store.go:
package platform
import "context"
type Store interface {
DashboardSummary(context.Context) (DashboardSummary, error)
ListVehicles(context.Context, VehicleQuery) (Page[VehicleRow], error)
}
type MockStore struct {
vehicles []VehicleRow
}
func NewMockStore() *MockStore {
return &MockStore{vehicles: []VehicleRow{
{VIN: "LB9A32A24R0LS1426", Plate: "粤AG18312", Phone: "13307795425", OEM: "G7s", Protocol: "JT808", Online: true, LastSeen: "2026-07-03 20:00:00", LocationText: "广东 广州", BindingScore: 100},
{VIN: "LNXNEGRR7SR318212", Plate: "川A00001", Phone: "", OEM: "Hyundai", Protocol: "GB32960", Online: true, LastSeen: "2026-07-03 20:00:03", LocationText: "四川 成都", BindingScore: 80},
}}
}
func (s *MockStore) DashboardSummary(context.Context) (DashboardSummary, error) {
return DashboardSummary{
OnlineVehicles: 2,
ActiveToday: 2,
FrameToday: 138240,
IssueVehicles: 1,
KafkaLag: 0,
Protocols: []ProtocolStat{
{Protocol: "GB32960", Online: 1, Total: 1},
{Protocol: "JT808", Online: 1, Total: 1},
{Protocol: "YUTONG_MQTT", Online: 0, Total: 0},
},
LinkHealth: []LinkHealth{
{Name: "Gateway", Status: "ok"},
{Name: "NATS", Status: "ok"},
{Name: "Kafka", Status: "ok"},
{Name: "TDengine", Status: "ok"},
{Name: "Redis", Status: "ok"},
{Name: "MySQL", Status: "ok"},
},
}, nil
}
func (s *MockStore) ListVehicles(_ context.Context, query VehicleQuery) (Page[VehicleRow], error) {
limit := query.Limit
if limit <= 0 {
limit = 20
}
offset := query.Offset
if offset < 0 {
offset = 0
}
end := offset + limit
if end > len(s.vehicles) {
end = len(s.vehicles)
}
items := []VehicleRow{}
if offset < len(s.vehicles) {
items = s.vehicles[offset:end]
}
return Page[VehicleRow]{Items: items, Total: int64(len(s.vehicles)), Limit: limit, Offset: offset}, nil
}
- Step 5: Implement service
Create vehicle-data-platform/apps/api/internal/platform/service.go:
package platform
import "context"
type Service struct {
store Store
}
func NewService(store Store) *Service {
if store == nil {
store = NewMockStore()
}
return &Service{store: store}
}
func (s *Service) DashboardSummary(ctx context.Context) (DashboardSummary, error) {
return s.store.DashboardSummary(ctx)
}
func (s *Service) ListVehicles(ctx context.Context, query VehicleQuery) (Page[VehicleRow], error) {
if query.Limit <= 0 {
query.Limit = 20
}
if query.Limit > 200 {
query.Limit = 200
}
if query.Offset < 0 {
query.Offset = 0
}
return s.store.ListVehicles(ctx, query)
}
- Step 6: Run tests
Run:
cd vehicle-data-platform/apps/api
go test ./internal/platform
Expected: pass.
- Step 7: Commit
git add vehicle-data-platform/apps/api/internal/platform
git commit -m "feat(platform-api): add platform service model"
Task 5: Backend HTTP Routes And Static Serving
Files:
-
Create:
vehicle-data-platform/apps/api/internal/platform/handler.go -
Create:
vehicle-data-platform/apps/api/internal/platform/handler_test.go -
Create:
vehicle-data-platform/apps/api/internal/app/server.go -
Create:
vehicle-data-platform/apps/api/internal/app/routes.go -
Create:
vehicle-data-platform/apps/api/cmd/platform-api/main.go -
Step 1: Write handler tests
Create vehicle-data-platform/apps/api/internal/platform/handler_test.go:
package platform
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestHandlerDashboardSummary(t *testing.T) {
handler := NewHandler(NewService(NewMockStore()))
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/dashboard/summary", nil)
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
for _, want := range []string{"onlineVehicles", "protocols", "linkHealth"} {
if !strings.Contains(rec.Body.String(), want) {
t.Fatalf("response missing %s: %s", want, rec.Body.String())
}
}
}
func TestHandlerVehicleList(t *testing.T) {
handler := NewHandler(NewService(NewMockStore()))
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/vehicles?limit=1", nil)
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
for _, want := range []string{"LB9A32A24R0LS1426", "items", "total"} {
if !strings.Contains(rec.Body.String(), want) {
t.Fatalf("response missing %s: %s", want, rec.Body.String())
}
}
}
- Step 2: Run handler tests and see failure
Run:
cd vehicle-data-platform/apps/api
go test ./internal/platform -run Handler
Expected: fail because NewHandler is undefined.
- Step 3: Implement handler
Create vehicle-data-platform/apps/api/internal/platform/handler.go:
package platform
import (
"net/http"
"strconv"
"strings"
"lingniu-vehicle-platform/api/internal/httpx"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
if service == nil {
service = NewService(NewMockStore())
}
return &Handler{service: service}
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
traceID := r.Header.Get("X-Trace-Id")
if traceID == "" {
traceID = "local"
}
path := strings.Trim(r.URL.Path, "/")
switch {
case r.Method == http.MethodGet && path == "api/dashboard/summary":
data, err := h.service.DashboardSummary(r.Context())
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "QUERY_FAILED", "查询失败", err.Error(), traceID)
return
}
httpx.WriteOK(w, traceID, data)
case r.Method == http.MethodGet && path == "api/vehicles":
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
data, err := h.service.ListVehicles(r.Context(), VehicleQuery{
Keyword: r.URL.Query().Get("keyword"),
Protocol: r.URL.Query().Get("protocol"),
Online: r.URL.Query().Get("online"),
Limit: limit,
Offset: offset,
})
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "QUERY_FAILED", "查询失败", err.Error(), traceID)
return
}
httpx.WriteOK(w, traceID, data)
default:
httpx.WriteError(w, http.StatusNotFound, "NOT_FOUND", "接口不存在", path, traceID)
}
}
- Step 4: Implement app routes
Create vehicle-data-platform/apps/api/internal/app/routes.go:
package app
import (
"net/http"
"lingniu-vehicle-platform/api/internal/platform"
)
func NewMux(service *platform.Service, static http.Handler) *http.ServeMux {
mux := http.NewServeMux()
api := platform.NewHandler(service)
mux.Handle("/api/", api)
if static != nil {
mux.Handle("/", static)
}
return mux
}
Create vehicle-data-platform/apps/api/internal/app/server.go:
package app
import (
"net/http"
"time"
)
func NewServer(addr string, handler http.Handler) *http.Server {
return &http.Server{
Addr: addr,
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
}
}
- Step 5: Implement main
Create vehicle-data-platform/apps/api/cmd/platform-api/main.go:
package main
import (
"log"
"net/http"
"os"
"lingniu-vehicle-platform/api/internal/app"
"lingniu-vehicle-platform/api/internal/config"
"lingniu-vehicle-platform/api/internal/platform"
)
func main() {
cfg := config.Load()
service := platform.NewService(platform.NewMockStore())
static := http.FileServer(http.Dir(os.Getenv("STATIC_DIR")))
server := app.NewServer(cfg.HTTPAddr, app.NewMux(service, static))
log.Printf("vehicle platform api listening on %s", cfg.HTTPAddr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}
- Step 6: Run tests and build
Run:
cd vehicle-data-platform/apps/api
go test ./...
go build ./cmd/platform-api
Expected: pass.
- Step 7: Commit
git add vehicle-data-platform/apps/api
git commit -m "feat(platform-api): expose dashboard and vehicle APIs"
Task 6: Frontend App Shell With Semi UI
Files:
-
Create:
vehicle-data-platform/apps/web/src/main.tsx -
Create:
vehicle-data-platform/apps/web/src/App.tsx -
Create:
vehicle-data-platform/apps/web/src/layout/AppShell.tsx -
Create:
vehicle-data-platform/apps/web/src/components/PageHeader.tsx -
Create:
vehicle-data-platform/apps/web/src/components/StatusTag.tsx -
Create:
vehicle-data-platform/apps/web/src/components/DataEmpty.tsx -
Create:
vehicle-data-platform/apps/web/src/test/App.test.tsx -
Step 1: Write app shell test
Create vehicle-data-platform/apps/web/src/test/App.test.tsx:
import { render, screen } from '@testing-library/react';
import App from '../App';
test('renders vehicle data platform navigation', () => {
render(<App />);
expect(screen.getByText('车辆数据管理中台')).toBeInTheDocument();
expect(screen.getByText('总览工作台')).toBeInTheDocument();
expect(screen.getByText('车辆台账')).toBeInTheDocument();
expect(screen.getByText('实时监控')).toBeInTheDocument();
});
- Step 2: Run test and see failure
Run:
cd vehicle-data-platform/apps/web
npm run test
Expected: fail because App does not exist.
- Step 3: Implement entry and shell
Create vehicle-data-platform/apps/web/src/main.tsx:
import React from 'react';
import ReactDOM from 'react-dom/client';
import '@douyinfe/semi-ui/dist/css/semi.min.css';
import './styles/global.css';
import App from './App';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Create vehicle-data-platform/apps/web/src/App.tsx:
import { useState } from 'react';
import { Dashboard } from './pages/Dashboard';
import { Vehicles } from './pages/Vehicles';
import { Realtime } from './pages/Realtime';
import { History } from './pages/History';
import { Mileage } from './pages/Mileage';
import { Quality } from './pages/Quality';
import { VehicleDetail } from './pages/VehicleDetail';
import { AppShell, PageKey } from './layout/AppShell';
export default function App() {
const [page, setPage] = useState<PageKey>('dashboard');
return (
<AppShell active={page} onChange={setPage}>
{page === 'dashboard' && <Dashboard />}
{page === 'vehicles' && <Vehicles />}
{page === 'realtime' && <Realtime />}
{page === 'detail' && <VehicleDetail />}
{page === 'history' && <History />}
{page === 'mileage' && <Mileage />}
{page === 'quality' && <Quality />}
</AppShell>
);
}
Create vehicle-data-platform/apps/web/src/layout/AppShell.tsx:
import { Layout, Nav, Typography } from '@douyinfe/semi-ui';
import { IconHome, IconMapPin, IconServer, IconSetting, IconHistogram, IconSearch, IconList } from '@douyinfe/semi-icons';
import type { ReactNode } from 'react';
export type PageKey = 'dashboard' | 'vehicles' | 'realtime' | 'detail' | 'history' | 'mileage' | 'quality';
const items = [
{ itemKey: 'dashboard', text: '总览工作台', icon: <IconHome /> },
{ itemKey: 'vehicles', text: '车辆台账', icon: <IconList /> },
{ itemKey: 'realtime', text: '实时监控', icon: <IconMapPin /> },
{ itemKey: 'detail', text: '车辆详情', icon: <IconSearch /> },
{ itemKey: 'history', text: '历史数据', icon: <IconServer /> },
{ itemKey: 'mileage', text: '里程与统计', icon: <IconHistogram /> },
{ itemKey: 'quality', text: '数据质量与链路健康', icon: <IconSetting /> }
];
export function AppShell({ active, onChange, children }: { active: PageKey; onChange: (key: PageKey) => void; children: ReactNode }) {
return (
<Layout style={{ minHeight: '100vh' }}>
<Layout.Sider style={{ width: 'var(--vp-shell-sidebar)', background: '#fff', borderRight: '1px solid var(--vp-border)' }}>
<div style={{ height: 56, display: 'flex', alignItems: 'center', padding: '0 20px', borderBottom: '1px solid var(--vp-border)' }}>
<Typography.Text strong>车辆数据管理中台</Typography.Text>
</div>
<Nav selectedKeys={[active]} items={items} onSelect={({ itemKey }) => onChange(itemKey as PageKey)} />
</Layout.Sider>
<Layout>
<Layout.Header style={{ height: 56, background: '#fff', borderBottom: '1px solid var(--vp-border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 24px' }}>
<Typography.Text type="tertiary">生产环境 · 自动刷新 60s</Typography.Text>
<Typography.Text type="tertiary">ECS 115.29.187.205</Typography.Text>
</Layout.Header>
<Layout.Content>{children}</Layout.Content>
</Layout>
</Layout>
);
}
- Step 4: Implement shared components
Create vehicle-data-platform/apps/web/src/components/PageHeader.tsx:
import { Typography } from '@douyinfe/semi-ui';
export function PageHeader({ title, description }: { title: string; description: string }) {
return (
<div style={{ marginBottom: 16 }}>
<Typography.Title heading={3} style={{ margin: 0 }}>{title}</Typography.Title>
<Typography.Text type="tertiary">{description}</Typography.Text>
</div>
);
}
Create vehicle-data-platform/apps/web/src/components/StatusTag.tsx:
import { Tag } from '@douyinfe/semi-ui';
export function StatusTag({ status }: { status: 'ok' | 'warning' | 'error' | 'offline' }) {
const map = {
ok: { color: 'green' as const, text: '正常' },
warning: { color: 'orange' as const, text: '关注' },
error: { color: 'red' as const, text: '异常' },
offline: { color: 'grey' as const, text: '离线' }
};
return <Tag color={map[status].color}>{map[status].text}</Tag>;
}
Create vehicle-data-platform/apps/web/src/components/DataEmpty.tsx:
import { Empty } from '@douyinfe/semi-ui';
export function DataEmpty({ text = '暂无数据' }: { text?: string }) {
return <Empty description={text} style={{ padding: 48 }} />;
}
- Step 5: Create first-pass page components
Create vehicle-data-platform/apps/web/src/pages/Dashboard.tsx:
import { PageHeader } from '../components/PageHeader';
export function Dashboard() {
return (
<div className="vp-page">
<PageHeader title="总览工作台" description="车辆在线、协议分布、数据质量和链路健康的统一入口" />
<div className="vp-section" style={{ padding: 24 }}>加载总览数据...</div>
</div>
);
}
Create the remaining page files with the exact title and description below. Each file imports PageHeader, exports a function named after the file, wraps content in <div className="vp-page">, and renders one <div className="vp-section" style={{ padding: 24 }}> with the listed loading text.
| File | Export | Title | Description | Loading text |
|---|---|---|---|---|
Vehicles.tsx |
Vehicles |
车辆台账 |
车辆身份、协议绑定、车牌、手机号和 OEM 的运营台账 |
加载车辆台账... |
Realtime.tsx |
Realtime |
实时状态 |
按协议和车辆查看最新实时位置、在线状态和核心数据 |
加载实时状态... |
VehicleDetail.tsx |
VehicleDetail |
车辆详情 |
单车身份、实时、历史、RAW、里程和质量的综合视图 |
加载车辆详情... |
History.tsx |
History |
历史查询 |
位置历史和 RAW 帧历史的分页查询工作台 |
加载历史查询... |
Mileage.tsx |
Mileage |
里程分析 |
每日里程、区间里程和异常差值分析 |
加载里程分析... |
Quality.tsx |
Quality |
数据质量 |
断链、VIN 缺失、字段缺失和链路健康的排查入口 |
加载数据质量... |
- Step 6: Run tests and build
Run:
cd vehicle-data-platform/apps/web
npm run test
npm run build
Expected: pass.
- Step 7: Commit
git add vehicle-data-platform/apps/web/src
git commit -m "feat(platform-web): add Semi UI app shell"
Task 7: Frontend API Client And Dashboard
Files:
-
Create:
vehicle-data-platform/apps/web/src/api/types.ts -
Create:
vehicle-data-platform/apps/web/src/api/client.ts -
Modify:
vehicle-data-platform/apps/web/src/pages/Dashboard.tsx -
Step 1: Create API types
Create vehicle-data-platform/apps/web/src/api/types.ts:
export interface ApiEnvelope<T> {
data: T;
traceId: string;
timestamp: number;
}
export interface ProtocolStat {
protocol: string;
online: number;
total: number;
}
export interface LinkHealth {
name: string;
status: string;
detail?: string;
}
export interface DashboardSummary {
onlineVehicles: number;
activeToday: number;
frameToday: number;
issueVehicles: number;
kafkaLag: number;
protocols: ProtocolStat[];
linkHealth: LinkHealth[];
}
export interface VehicleRow {
vin: string;
plate: string;
phone: string;
oem: string;
protocol: string;
online: boolean;
lastSeen: string;
locationText: string;
bindingScore: number;
}
export interface Page<T> {
items: T[];
total: number;
limit: number;
offset: number;
}
- Step 2: Create API client
Create vehicle-data-platform/apps/web/src/api/client.ts:
import type { ApiEnvelope, DashboardSummary, Page, VehicleRow } from './types';
async function request<T>(path: string): Promise<T> {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`request failed ${response.status}`);
}
const envelope = (await response.json()) as ApiEnvelope<T>;
return envelope.data;
}
export const api = {
dashboardSummary: () => request<DashboardSummary>('/api/dashboard/summary'),
vehicles: (params: URLSearchParams) => request<Page<VehicleRow>>(`/api/vehicles?${params.toString()}`)
};
- Step 3: Update dashboard
Replace Dashboard.tsx with a complete data-bound page. It must import useEffect, useState, Semi UI Card, Col, Row, Spin, Table, Tag, and Toast, plus api, DashboardSummary, and PageHeader. The component state and loading lifecycle must be:
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.dashboardSummary()
.then(setSummary)
.catch(error => Toast.error(error.message))
.finally(() => setLoading(false));
}, []);
Render inside <Spin spinning={loading}>:
-
Four KPI cards: online vehicles, active today, frames today, issue vehicles.
-
A protocol table with columns
protocol,online,total. -
A link-health table with columns
name,status,detail; status rendersTagwith green forok, orange forwarning, red forerror, and grey for other values. -
A compact Kafka lag card showing
summary.kafkaLag. -
Step 4: Run frontend build
Run:
cd vehicle-data-platform/apps/web
npm run build
Expected: pass.
- Step 5: Commit
git add vehicle-data-platform/apps/web/src/api vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
git commit -m "feat(platform-web): connect dashboard summary"
Task 8: Vehicles And Realtime Pages
Files:
-
Modify:
vehicle-data-platform/apps/web/src/pages/Vehicles.tsx -
Modify:
vehicle-data-platform/apps/web/src/pages/Realtime.tsx -
Modify:
vehicle-data-platform/apps/api/internal/platform/model.go -
Modify:
vehicle-data-platform/apps/api/internal/platform/handler.go -
Step 1: Write backend test for realtime location route
Add to handler_test.go:
func TestHandlerRealtimeLocations(t *testing.T) {
handler := NewHandler(NewService(NewMockStore()))
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/realtime/locations?limit=10", nil)
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "LB9A32A24R0LS1426") {
t.Fatalf("response missing vehicle: %s", rec.Body.String())
}
}
- Step 2: Run backend test and see failure
Run:
cd vehicle-data-platform/apps/api
go test ./internal/platform -run RealtimeLocations
Expected: fail with 404.
- Step 3: Implement realtime location models and route
Add RealtimeLocationRow to model.go and expose GET /api/realtime/locations using mock data derived from vehicles.
Expected JSON fields:
{
"vin": "LB9A32A24R0LS1426",
"plate": "粤AG18312",
"protocol": "JT808",
"longitude": 113.2644,
"latitude": 23.1291,
"speedKmh": 42.5,
"socPercent": 78.4,
"totalMileageKm": 119925,
"lastSeen": "2026-07-03 20:00:00"
}
- Step 4: Implement Vehicles page
Use Semi UI Form, Input, Select, Button, Table, Drawer, and Tag. The table must show VIN, plate, phone, OEM, protocol, online status, lastSeen, locationText, and bindingScore.
- Step 5: Implement Realtime page
Use two tabs:
表格视图with realtime location table.地图视图with a professional schematic map panel that plots rows as simple positioned dots inside a bounded panel.
Do not use a full-screen decorative map.
- Step 6: Verify
Run:
cd vehicle-data-platform/apps/api && go test ./...
cd ../web && npm run build
- Step 7: Commit
git add vehicle-data-platform/apps/api vehicle-data-platform/apps/web/src/pages/Vehicles.tsx vehicle-data-platform/apps/web/src/pages/Realtime.tsx
git commit -m "feat(platform): add vehicles and realtime pages"
Task 9: History, Mileage, Quality, And Vehicle Detail Pages
Files:
-
Modify:
vehicle-data-platform/apps/api/internal/platform/model.go -
Modify:
vehicle-data-platform/apps/api/internal/platform/mock_store.go -
Modify:
vehicle-data-platform/apps/api/internal/platform/service.go -
Modify:
vehicle-data-platform/apps/api/internal/platform/handler.go -
Modify:
vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx -
Modify:
vehicle-data-platform/apps/web/src/pages/History.tsx -
Modify:
vehicle-data-platform/apps/web/src/pages/Mileage.tsx -
Modify:
vehicle-data-platform/apps/web/src/pages/Quality.tsx -
Step 1: Add backend tests
Add handler tests for:
GET /api/history/locationsPOST /api/history/raw-frames/queryGET /api/mileage/dailyGET /api/quality/issuesGET /api/ops/health
Each test should assert HTTP 200 and a domain-specific field, such as rawSizeBytes, dailyMileageKm, issueType, or linkHealth.
- Step 2: Run backend tests and see failure
Run:
cd vehicle-data-platform/apps/api
go test ./internal/platform -run 'History|Mileage|Quality|Ops'
Expected: fail because routes are missing.
- Step 3: Implement mock-backed routes
Implement mock-backed service methods and routes for all five endpoints with these concrete response models:
HistoryLocationRow:vin,plate,protocol,longitude,latitude,speedKmh,totalMileageKm,deviceTime,serverTime.RawFrameRow:id,vin,protocol,frameType,deviceTime,serverTime,rawSizeBytes,parsedFields.DailyMileageRow:vin,plate,date,startMileageKm,endMileageKm,dailyMileageKm,source.QualityIssueRow:vin,plate,protocol,issueType,severity,lastSeen,detail.OpsHealth:linkHealth,kafkaLag,redisOnlineKeys,tdengineWritable,mysqlWritable.
The mock store returns at least two rows per table so frontend empty and non-empty table states can both be verified by changing query filters.
- Step 4: Implement pages
Use Semi UI:
VehicleDetail:Tabs,Descriptions,Table,CodeHighlightstyle JSON block.History: filters,Tabs, table, raw detail drawer.Mileage: date range controls, daily mileage table, anomaly tags.Quality: issue summary cards, issue table, ops health table.
Each page must call its matching API endpoint through src/api/client.ts, show a loading state, show DataEmpty when no rows are returned, and render Toast.error(error.message) on request failure.
- Step 5: Verify
Run:
cd vehicle-data-platform/apps/api && go test ./...
cd ../web && npm run build
- Step 6: Commit
git add vehicle-data-platform/apps/api vehicle-data-platform/apps/web/src/pages
git commit -m "feat(platform): add history mileage quality workflows"
Task 10: Replace Mock Store With Production Repositories
Files:
-
Create:
vehicle-data-platform/apps/api/internal/platform/mysql_store.go -
Create:
vehicle-data-platform/apps/api/internal/platform/redis_store.go -
Create:
vehicle-data-platform/apps/api/internal/platform/tdengine_store.go -
Modify:
vehicle-data-platform/apps/api/cmd/platform-api/main.go -
Step 1: Write repository query-builder tests
Do not add a SQL mocking dependency. Create pure query-builder functions and unit test SQL strings plus argument order.
Create tests for these query builders:
-
Vehicle list from
vehicle_identity_bindingplus realtime location. -
Mileage daily from
vehicle_daily_mileage. -
Raw frame query from TDengine
raw_frames. -
Step 2: Implement production store
Create a ProductionStore that satisfies platform.Store. It should:
-
Use MySQL for vehicle list, realtime snapshot, realtime location, mileage, identity.
-
Use TDengine for raw frames and history locations.
-
Use Redis for realtime raw and online state.
-
Use capacity-check executable or local metrics endpoints for ops health.
-
Step 3: Keep mock fallback
In main.go, if production DSNs are empty, use NewMockStore(). If DSNs are set, use NewProductionStore(...). This makes local UI development possible without production credentials.
- Step 4: Verify
Run:
cd vehicle-data-platform/apps/api
go test ./...
- Step 5: Commit
git add vehicle-data-platform/apps/api
git commit -m "feat(platform-api): connect production data stores"
Task 11: Static Serving, Deployment, And Systemd
Files:
-
Create:
vehicle-data-platform/deploy/systemd/lingniu-vehicle-platform.service -
Create:
vehicle-data-platform/docs/deployment.md -
Modify:
vehicle-data-platform/package.json -
Modify:
vehicle-data-platform/apps/api/cmd/platform-api/main.go -
Step 1: Create systemd unit
Create vehicle-data-platform/deploy/systemd/lingniu-vehicle-platform.service:
[Unit]
Description=Lingniu Vehicle Data Platform
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/lingniu-vehicle-platform/current
EnvironmentFile=/opt/lingniu-vehicle-platform/env/platform.env
ExecStart=/opt/lingniu-vehicle-platform/current/platform-api
Restart=always
RestartSec=3
LimitNOFILE=1048576
KillSignal=SIGTERM
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
- Step 2: Write deployment doc
Create vehicle-data-platform/docs/deployment.md with:
# Deployment
## Build
Run from repository root:
```bash
cd vehicle-data-platform
npm install
npm --prefix apps/web install
npm run web:build
cd apps/api
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o ../../dist/platform-api ./cmd/platform-api
```
## ECS Paths
```text
/opt/lingniu-vehicle-platform/current
/opt/lingniu-vehicle-platform/releases
/opt/lingniu-vehicle-platform/env/platform.env
```
## Health
```bash
curl -fsS http://127.0.0.1:20300/api/ops/health
```
- Step 3: Verify build
Run:
cd vehicle-data-platform
npm run web:build
cd apps/api
go test ./...
go build -o ../../dist/platform-api ./cmd/platform-api
- Step 4: Commit
git add vehicle-data-platform/deploy vehicle-data-platform/docs vehicle-data-platform/package.json vehicle-data-platform/apps/api/cmd/platform-api/main.go
git commit -m "ops(platform): add deployment unit and docs"
Task 12: ECS Deploy And Browser Verification
Files:
-
Modify only if deployment reveals a concrete defect.
-
Step 1: Build release locally
Run:
cd vehicle-data-platform
npm install
npm --prefix apps/web install
npm run web:build
cd apps/api
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o ../../dist/platform-api ./cmd/platform-api
- Step 2: Upload release
Run:
release="platform-$(date +%Y%m%d%H%M%S)"
ssh root@115.29.187.205 "mkdir -p /opt/lingniu-vehicle-platform/releases/$release /opt/lingniu-vehicle-platform/env"
scp vehicle-data-platform/dist/platform-api root@115.29.187.205:/opt/lingniu-vehicle-platform/releases/$release/
scp -r vehicle-data-platform/apps/web/dist root@115.29.187.205:/opt/lingniu-vehicle-platform/releases/$release/web
scp vehicle-data-platform/deploy/systemd/lingniu-vehicle-platform.service root@115.29.187.205:/etc/systemd/system/
ssh root@115.29.187.205 "chmod 755 /opt/lingniu-vehicle-platform/releases/$release/platform-api && ln -sfn /opt/lingniu-vehicle-platform/releases/$release /opt/lingniu-vehicle-platform/current"
- Step 3: Create environment file
Create /opt/lingniu-vehicle-platform/env/platform.env on ECS with production DSNs. Do not commit secrets.
Required keys:
HTTP_ADDR=:20300
STATIC_DIR=/opt/lingniu-vehicle-platform/current/web
MYSQL_DSN=...
REDIS_ADDR=...
REDIS_USERNAME=...
REDIS_PASSWORD=...
REDIS_DB=50
TDENGINE_DSN=...
TDENGINE_DATABASE=lingniu_vehicle_ts
CAPACITY_CHECK_BIN=/opt/lingniu-go-native/current/capacity-check
AUTH_TOKEN=...
- Step 4: Start service
Run:
ssh root@115.29.187.205 "systemctl daemon-reload && systemctl enable --now lingniu-vehicle-platform.service && systemctl status lingniu-vehicle-platform.service --no-pager"
- Step 5: Verify APIs
Run:
curl -fsS http://127.0.0.1:20300/api/dashboard/summary
curl -fsS 'http://127.0.0.1:20300/api/vehicles?limit=5'
curl -fsS http://127.0.0.1:20300/api/ops/health
Expected:
-
All return JSON envelopes.
-
Dashboard has
onlineVehicles. -
Vehicles has
items. -
Ops health has link health state.
-
Step 6: Browser verification
Open:
http://115.29.187.205:20300
Verify:
-
Dashboard loads.
-
Vehicles page table loads.
-
Realtime page loads.
-
History page renders query controls.
-
Mileage page renders tables.
-
Quality page renders issue and health sections.
-
Visual style is Semi UI console style: clean, dense, professional, no marketing hero.
-
Step 7: Commit deployment fixes
If deployment required code changes:
git add vehicle-data-platform
git commit -m "fix(platform): stabilize ecs deployment"
Self Review
Spec coverage:
- New project folder: Task 2 creates
vehicle-data-platform. - Semi UI frontend: Tasks 1, 2, 6, 7, 8, 9.
- Go backend: Tasks 2, 3, 4, 5, 10.
- Dashboard, vehicles, realtime, detail, history, mileage, quality: Tasks 6 through 9.
- RAW query with POST and field filtering: Tasks 9 and 10.
- Production data sources: Task 10.
- ECS deployment and browser testing: Tasks 11 and 12.
Completeness scan:
- This plan contains concrete files, routes, models, verification commands, and commit points.
- Production secrets are intentionally described as environment values and must not be committed.
Type consistency:
DashboardSummary,VehicleRow,Page<T>, and API envelope names match between backend and frontend tasks.- API route names match the design spec.