diff --git a/vehicle-data-platform/apps/api/internal/platform/model.go b/vehicle-data-platform/apps/api/internal/platform/model.go
index b1c9656d..7f5d3c39 100644
--- a/vehicle-data-platform/apps/api/internal/platform/model.go
+++ b/vehicle-data-platform/apps/api/internal/platform/model.go
@@ -285,6 +285,7 @@ type OpsHealth struct {
LinkHealth []LinkHealth `json:"linkHealth"`
KafkaLag *int `json:"kafkaLag"`
ActiveConnections *int `json:"activeConnections"`
+ CapacityFindings []string `json:"capacityFindings"`
RedisOnlineKeys *int `json:"redisOnlineKeys"`
TDengineWritable bool `json:"tdengineWritable"`
MySQLWritable bool `json:"mysqlWritable"`
diff --git a/vehicle-data-platform/apps/api/internal/platform/production_store.go b/vehicle-data-platform/apps/api/internal/platform/production_store.go
index 566edf58..12395b73 100644
--- a/vehicle-data-platform/apps/api/internal/platform/production_store.go
+++ b/vehicle-data-platform/apps/api/internal/platform/production_store.go
@@ -905,7 +905,7 @@ func (s *ProductionStore) OpsHealth(ctx context.Context) (OpsHealth, error) {
locationHealth := s.mysqlTableReadHealth(ctx, "vehicle_realtime_location", "读取实时位置表正常")
tdengineHealth := s.tdengineRawFrameHealth(ctx)
redisHealth, redisOnlineKeys := s.redisOnlineKeyHealth(ctx)
- capacityHealth, kafkaLag, activeConnections := s.capacityCheckHealth(ctx)
+ capacityHealth, kafkaLag, activeConnections, capacityFindings := s.capacityCheckHealth(ctx)
mysqlWritable := mysqlStatus == "ok" && snapshotHealth.Status == "ok" && locationHealth.Status == "ok"
return OpsHealth{
LinkHealth: []LinkHealth{
@@ -918,22 +918,23 @@ func (s *ProductionStore) OpsHealth(ctx context.Context) (OpsHealth, error) {
},
KafkaLag: kafkaLag,
ActiveConnections: activeConnections,
+ CapacityFindings: capacityFindings,
RedisOnlineKeys: redisOnlineKeys,
TDengineWritable: tdengineHealth.Status == "ok",
MySQLWritable: mysqlWritable,
}, nil
}
-func (s *ProductionStore) capacityCheckHealth(ctx context.Context) (LinkHealth, *int, *int) {
+func (s *ProductionStore) capacityCheckHealth(ctx context.Context) (LinkHealth, *int, *int, []string) {
health := LinkHealth{Name: "Capacity check", Status: "warning", Detail: "平台暂未接入 capacity-check"}
if s.capacityCheck == nil {
- return health, nil, nil
+ return health, nil, nil, []string{}
}
result, err := s.capacityCheck.CheckCapacity(ctx)
if err != nil {
health.Status = "error"
health.Detail = err.Error()
- return health, nil, nil
+ return health, nil, nil, []string{}
}
kafkaLag := int(result.KafkaLag)
activeConnections := int(result.ActiveConnections)
@@ -947,7 +948,7 @@ func (s *ProductionStore) capacityCheckHealth(ctx context.Context) (LinkHealth,
health.Status = "warning"
}
health.Detail = detail
- return health, &kafkaLag, &activeConnections
+ return health, &kafkaLag, &activeConnections, append([]string{}, result.Findings...)
}
func (s *ProductionStore) redisOnlineKeyHealth(ctx context.Context) (LinkHealth, *int) {
diff --git a/vehicle-data-platform/apps/api/internal/platform/production_store_test.go b/vehicle-data-platform/apps/api/internal/platform/production_store_test.go
index 5b45fec9..732c1c53 100644
--- a/vehicle-data-platform/apps/api/internal/platform/production_store_test.go
+++ b/vehicle-data-platform/apps/api/internal/platform/production_store_test.go
@@ -230,6 +230,9 @@ func TestOpsHealthUsesCapacityCheckProbeForKafkaLag(t *testing.T) {
if health.ActiveConnections == nil || *health.ActiveConnections != 120000 {
t.Fatalf("OpsHealth should expose capacity-check active connections, got %+v", health.ActiveConnections)
}
+ if len(health.CapacityFindings) != 1 || health.CapacityFindings[0] != "kafka lag 42" {
+ t.Fatalf("OpsHealth should expose structured capacity findings, got %+v", health.CapacityFindings)
+ }
var capacityHealth *LinkHealth
for index := range health.LinkHealth {
if health.LinkHealth[index].Name == "Capacity check" {
@@ -242,6 +245,30 @@ func TestOpsHealthUsesCapacityCheckProbeForKafkaLag(t *testing.T) {
}
}
+func TestOpsHealthReturnsEmptyCapacityFindingsArray(t *testing.T) {
+ db, err := sql.Open("ops_health_test", "")
+ if err != nil {
+ t.Fatalf("open ops health db: %v", err)
+ }
+ defer db.Close()
+ store := &ProductionStore{
+ db: db,
+ capacityCheck: fakeCapacityCheckProbe{result: CapacityCheckResult{
+ Status: "ok",
+ ActiveConnections: 254,
+ KafkaLag: 0,
+ }},
+ }
+
+ health, err := store.OpsHealth(context.Background())
+ if err != nil {
+ t.Fatalf("OpsHealth returned error: %v", err)
+ }
+ if health.CapacityFindings == nil || len(health.CapacityFindings) != 0 {
+ t.Fatalf("OpsHealth should expose an empty capacity findings array, got %#v", health.CapacityFindings)
+ }
+}
+
func TestVehicleServiceOverviewMatchesPartialKeyword(t *testing.T) {
overview := VehicleServiceOverview{
VIN: "LB9A32A24R0LS1426",
diff --git a/vehicle-data-platform/apps/web/src/api/types.ts b/vehicle-data-platform/apps/web/src/api/types.ts
index 9ed4036a..932b7497 100644
--- a/vehicle-data-platform/apps/web/src/api/types.ts
+++ b/vehicle-data-platform/apps/web/src/api/types.ts
@@ -275,6 +275,7 @@ export interface OpsHealth {
linkHealth: LinkHealth[];
kafkaLag: number | null;
activeConnections: number | null;
+ capacityFindings: string[];
redisOnlineKeys: number | null;
tdengineWritable: boolean;
mysqlWritable: boolean;
diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx
index 813a295a..02253711 100644
--- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx
+++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx
@@ -198,6 +198,15 @@ export function Quality({
+ {health?.capacityFindings?.length ? (
+
+
+ {health.capacityFindings.map((item) => (
+ {item}
+ ))}
+
+
+ ) : null}
{filterSummary.length > 0 ? (
diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx
index 5f6a162c..1835c28e 100644
--- a/vehicle-data-platform/apps/web/src/test/App.test.tsx
+++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx
@@ -4153,6 +4153,31 @@ test('quality health shows active connection capacity metric', async () => {
expect(screen.getByText('120,000')).toBeInTheDocument();
});
+test('quality health shows structured capacity findings', async () => {
+ window.history.replaceState(null, '', '/#/quality');
+ vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
+ const path = String(input);
+ return {
+ ok: true,
+ json: async () => ({
+ data: path.includes('/api/ops/health')
+ ? { linkHealth: [], kafkaLag: 42, activeConnections: 120000, capacityFindings: ['kafka lag 42'], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }
+ : path.includes('/api/quality/summary')
+ ? { issueVehicleCount: 0, issueRecordCount: 0, errorCount: 0, warningCount: 0, protocols: [], issueTypes: [] }
+ : { items: [], total: 0, limit: 20, offset: 0 },
+ traceId: 'trace-test',
+ timestamp: 1783094400000
+ })
+ } as Response;
+ });
+
+ render();
+
+ expect(await screen.findByRole('heading', { name: '质量治理' })).toBeInTheDocument();
+ expect(await screen.findByText('容量检查发现')).toBeInTheDocument();
+ expect(screen.getByText('kafka lag 42')).toBeInTheDocument();
+});
+
test('shows vehicle service evidence chain before source details', async () => {
window.history.replaceState(null, '', '/#/detail?keyword=VIN001');
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {