diff --git a/vehicle-data-platform/apps/api/internal/platform/mysql_queries.go b/vehicle-data-platform/apps/api/internal/platform/mysql_queries.go index e044a98f..6f8c8c7d 100644 --- a/vehicle-data-platform/apps/api/internal/platform/mysql_queries.go +++ b/vehicle-data-platform/apps/api/internal/platform/mysql_queries.go @@ -146,8 +146,7 @@ func buildVehicleCoverageSQL(query url.Values) SQLQuery { if len(having) > 0 { havingSQL = ` HAVING ` + strings.Join(having, " AND ") + ` ` } - vehicleSetSQL := `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> '' ` + - `UNION SELECT vin FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> ''` + vehicleSetSQL := `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> ''` groupSQL := `FROM (` + vehicleSetSQL + `) v ` + `LEFT JOIN vehicle_identity_binding b ON b.vin = v.vin ` + `LEFT JOIN vehicle_realtime_snapshot s ON s.vin = v.vin ` + @@ -246,8 +245,7 @@ func buildVehicleCoverageSummarySQL(query url.Values) SQLQuery { if len(having) > 0 { havingSQL = ` HAVING ` + strings.Join(having, " AND ") + ` ` } - vehicleSetSQL := `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> '' ` + - `UNION SELECT vin FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> ''` + vehicleSetSQL := `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> ''` groupSQL := `SELECT v.vin, ` + `COUNT(DISTINCT s.protocol) AS source_count, ` + `COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) AS online_source_count, ` + 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 87de5b3c..8c8b9013 100644 --- a/vehicle-data-platform/apps/api/internal/platform/production_store.go +++ b/vehicle-data-platform/apps/api/internal/platform/production_store.go @@ -132,22 +132,26 @@ func (s *ProductionStore) serviceStatusStats(ctx context.Context) ([]ServiceStat return out, nil } -func (s *ProductionStore) VehicleServiceSummary(ctx context.Context) (VehicleServiceSummary, error) { +func buildVehicleServiceSummarySQL() string { canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols)) - rows, err := s.db.QueryContext(ctx, `SELECT + return `SELECT COUNT(*) AS total_vehicles, COALESCE(SUM(CASE WHEN bound = 1 THEN 1 ELSE 0 END), 0) AS bound_vehicles, COALESCE(SUM(CASE WHEN online_source_count > 0 THEN 1 ELSE 0 END), 0) AS online_vehicles, COALESCE(SUM(CASE WHEN source_count = 1 THEN 1 ELSE 0 END), 0) AS single_source_vehicles, COALESCE(SUM(CASE WHEN source_count > 1 THEN 1 ELSE 0 END), 0) AS multi_source_vehicles, COALESCE(SUM(CASE WHEN source_count = 0 THEN 1 ELSE 0 END), 0) AS no_data_vehicles, -COALESCE(SUM(CASE WHEN bound = 0 THEN 1 ELSE 0 END), 0) AS identity_required_vehicles, +(SELECT COUNT(DISTINCT unbound_source.vin) + FROM vehicle_realtime_snapshot unbound_source + LEFT JOIN vehicle_identity_binding bound_identity ON bound_identity.vin = unbound_source.vin + WHERE unbound_source.vin IS NOT NULL AND unbound_source.vin <> '' + AND bound_identity.vin IS NULL) AS identity_required_vehicles, COALESCE(SUM(CASE WHEN archive_complete = 0 THEN 1 ELSE 0 END), 0) AS archive_incomplete_vehicles, COALESCE(SUM(CASE WHEN missing_plate = 1 THEN 1 ELSE 0 END), 0) AS missing_plate_vehicles, COALESCE(SUM(CASE WHEN missing_phone = 1 THEN 1 ELSE 0 END), 0) AS missing_phone_vehicles, COALESCE(SUM(CASE WHEN missing_oem = 1 THEN 1 ELSE 0 END), 0) AS missing_oem_vehicles, -COALESCE(SUM(CASE WHEN bound = 1 AND source_count = `+canonicalSourceCount+` AND online_source_count = source_count THEN 1 ELSE 0 END), 0) AS healthy_vehicles, -COALESCE(SUM(CASE WHEN bound = 1 AND source_count > 0 AND online_source_count > 0 AND (online_source_count < source_count OR source_count < `+canonicalSourceCount+`) THEN 1 ELSE 0 END), 0) AS degraded_vehicles, +COALESCE(SUM(CASE WHEN bound = 1 AND source_count = ` + canonicalSourceCount + ` AND online_source_count = source_count THEN 1 ELSE 0 END), 0) AS healthy_vehicles, +COALESCE(SUM(CASE WHEN bound = 1 AND source_count > 0 AND online_source_count > 0 AND (online_source_count < source_count OR source_count < ` + canonicalSourceCount + `) THEN 1 ELSE 0 END), 0) AS degraded_vehicles, COALESCE(SUM(CASE WHEN bound = 1 AND source_count > 0 AND online_source_count = 0 THEN 1 ELSE 0 END), 0) AS offline_vehicles FROM ( SELECT v.vin, @@ -164,13 +168,15 @@ CASE WHEN NULLIF(MAX(NULLIF(b.phone, '')), '') IS NULL THEN 1 ELSE 0 END AS miss CASE WHEN NULLIF(MAX(NULLIF(b.oem, '')), '') IS NULL THEN 1 ELSE 0 END AS missing_oem FROM ( SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> '' -UNION -SELECT vin FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> '' ) v LEFT JOIN vehicle_identity_binding b ON b.vin = v.vin LEFT JOIN vehicle_realtime_snapshot s ON s.vin = v.vin GROUP BY v.vin -) vehicle_service_summary`) +) vehicle_service_summary` +} + +func (s *ProductionStore) VehicleServiceSummary(ctx context.Context) (VehicleServiceSummary, error) { + rows, err := s.db.QueryContext(ctx, buildVehicleServiceSummarySQL()) if err != nil { return VehicleServiceSummary{}, err } @@ -223,9 +229,7 @@ GROUP BY v.vin } func (s *ProductionStore) missingSourceStats(ctx context.Context) ([]MissingSourceStat, error) { - const vehicleSetSQL = `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> '' -UNION -SELECT vin FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> ''` + const vehicleSetSQL = `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> ''` out := make([]MissingSourceStat, 0, len(canonicalVehicleProtocols)) for _, protocol := range canonicalVehicleProtocols { var count int diff --git a/vehicle-data-platform/apps/api/internal/platform/query_builders_test.go b/vehicle-data-platform/apps/api/internal/platform/query_builders_test.go index 20cb670b..0872a1f7 100644 --- a/vehicle-data-platform/apps/api/internal/platform/query_builders_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/query_builders_test.go @@ -90,7 +90,6 @@ func TestBuildVehicleCoverageSQLIncludesNoDataVehicles(t *testing.T) { built := buildVehicleCoverageSQL(query) for _, want := range []string{ "vehicle_identity_binding", - "UNION", "vehicle_realtime_snapshot", "LEFT JOIN vehicle_realtime_snapshot s ON s.vin = v.vin", "COUNT(DISTINCT s.protocol) = 0", @@ -100,6 +99,10 @@ func TestBuildVehicleCoverageSQLIncludesNoDataVehicles(t *testing.T) { t.Fatalf("no-data coverage SQL missing %q: %s / %s", want, built.Text, built.CountText) } } + if strings.Contains(built.Text, "UNION SELECT vin FROM vehicle_realtime_snapshot") || + strings.Contains(built.CountText, "UNION SELECT vin FROM vehicle_realtime_snapshot") { + t.Fatalf("formal vehicle coverage must not promote unbound realtime identities into the vehicle population: %s / %s", built.Text, built.CountText) + } } func TestBuildVehicleCoverageSQLFiltersMissingProtocol(t *testing.T) { @@ -173,11 +176,32 @@ func TestBuildVehicleCoverageSummarySQL(t *testing.T) { if strings.Contains(built.Text, "LIMIT") || strings.Contains(built.Text, "OFFSET") { t.Fatalf("summary SQL should not paginate: %s", built.Text) } + if strings.Contains(built.Text, "UNION SELECT vin FROM vehicle_realtime_snapshot") { + t.Fatalf("formal vehicle coverage summary must use the authoritative identity population: %s", built.Text) + } if len(built.Args) != 6 || built.Args[0] != "%粤A%" { t.Fatalf("args = %#v", built.Args) } } +func TestBuildVehicleServiceSummarySQLKeepsFormalAndUnboundPopulationsSeparate(t *testing.T) { + built := buildVehicleServiceSummarySQL() + for _, want := range []string{ + "SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> ''", + "SELECT COUNT(DISTINCT unbound_source.vin)", + "LEFT JOIN vehicle_identity_binding bound_identity ON bound_identity.vin = unbound_source.vin", + "bound_identity.vin IS NULL", + } { + if !strings.Contains(built, want) { + t.Fatalf("vehicle service summary SQL missing %q: %s", want, built) + } + } + if strings.Contains(built, "UNION\nSELECT vin FROM vehicle_realtime_snapshot") || + strings.Contains(built, "UNION SELECT vin FROM vehicle_realtime_snapshot") { + t.Fatalf("vehicle service total must not include unbound realtime-only identities: %s", built) + } +} + func TestBuildRealtimeLocationSQL(t *testing.T) { query := url.Values{"vin": {"粤A"}, "protocol": {"GB32960"}, "limit": {"10"}, "offset": {"20"}} built := buildRealtimeLocationSQL(query) diff --git a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx index e821cef2..2b58dd91 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx @@ -76,13 +76,13 @@ test('reconciles service identities with bound and identity-required vehicles', runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false } }); mocks.sourceReadiness.mockResolvedValue({ - totalVehicles: 1035, boundVehicles: 1024, identityRequiredVehicles: 11, onlineVehicles: 234, + totalVehicles: 1024, boundVehicles: 1024, identityRequiredVehicles: 11, onlineVehicles: 234, kafkaLag: 0, activeConnections: 10, redisOnlineKeys: 5, platformRelease: 'test-release', sources: [] }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); - expect(await screen.findByText('1035 / 234')).toBeInTheDocument(); + expect(await screen.findByText('1024 / 234')).toBeInTheDocument(); for (const key of [['ops-health-v2'], ['ops-source-readiness-v2']]) { const query = client.getQueryCache().find({ queryKey: key }); const liveOptions = query?.options as { refetchIntervalInBackground?: boolean; refetchOnWindowFocus?: boolean }; @@ -102,7 +102,7 @@ test('keeps health evidence visible when source readiness fails and retries that runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false } }); mocks.sourceReadiness.mockRejectedValueOnce(new Error('来源就绪度暂时不可用')).mockResolvedValueOnce({ - totalVehicles: 1035, boundVehicles: 1024, identityRequiredVehicles: 11, onlineVehicles: 234, + totalVehicles: 1024, boundVehicles: 1024, identityRequiredVehicles: 11, onlineVehicles: 234, kafkaLag: 0, activeConnections: 10, redisOnlineKeys: 5, platformRelease: 'test-release', sources: [] }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); @@ -112,7 +112,7 @@ test('keeps health evidence visible when source readiness fails and retries that expect(screen.getByText('test-release')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /重试/ })); - expect(await screen.findByText('1035 / 234')).toBeInTheDocument(); + expect(await screen.findByText('1024 / 234')).toBeInTheDocument(); expect(screen.queryByText('来源就绪度暂时不可用')).not.toBeInTheDocument(); expect(mocks.sourceReadiness).toHaveBeenCalledTimes(2); }); diff --git a/vehicle-data-platform/deploy/migrations/018_vehicle_oem_audit.sql b/vehicle-data-platform/deploy/migrations/018_vehicle_oem_audit.sql new file mode 100644 index 00000000..a9c657a6 --- /dev/null +++ b/vehicle-data-platform/deploy/migrations/018_vehicle_oem_audit.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS vehicle_identity_oem_audit ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + vin VARCHAR(64) NOT NULL, + before_oem VARCHAR(128) NOT NULL DEFAULT '', + after_oem VARCHAR(128) NOT NULL, + source_system VARCHAR(64) NOT NULL, + source_version VARCHAR(128) NOT NULL, + source_brand_code VARCHAR(128) NOT NULL DEFAULT '', + source_model_name VARCHAR(255) NOT NULL DEFAULT '', + source_updated_at DATETIME(3) NULL, + actor VARCHAR(128) NOT NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + UNIQUE KEY uk_vehicle_oem_audit_version (vin, source_system, source_version), + INDEX idx_vehicle_oem_audit_created (created_at), + INDEX idx_vehicle_oem_audit_source (source_system, source_version) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/vehicle-data-platform/docs/deployment.md b/vehicle-data-platform/docs/deployment.md index 2400dae5..47b54280 100644 --- a/vehicle-data-platform/docs/deployment.md +++ b/vehicle-data-platform/docs/deployment.md @@ -202,13 +202,18 @@ test -n "$MYSQL_DSN" /opt/lingniu-vehicle-platform/releases/$PLATFORM_RELEASE/deploy/migrations/014_customer_vehicle_grant_time.sql \ /opt/lingniu-vehicle-platform/releases/$PLATFORM_RELEASE/deploy/migrations/015_vehicle_source_policy_audit.sql \ /opt/lingniu-vehicle-platform/releases/$PLATFORM_RELEASE/deploy/migrations/016_business_scope_dimensions.sql \ - /opt/lingniu-vehicle-platform/releases/$PLATFORM_RELEASE/deploy/migrations/017_reconciliation_center.sql + /opt/lingniu-vehicle-platform/releases/$PLATFORM_RELEASE/deploy/migrations/017_reconciliation_center.sql \ + /opt/lingniu-vehicle-platform/releases/$PLATFORM_RELEASE/deploy/migrations/018_vehicle_oem_audit.sql ``` -The API guards the access-threshold tables for compatibility, while alert and reconciliation APIs deliberately require their migrations to exist. Run every numbered migration explicitly before switching traffic so DDL permission and index creation failures are caught early. The migration journal records filename and SHA-256 and refuses a changed file; duplicate forward `ADD COLUMN` and `CREATE INDEX` statements are tolerated only when resuming partially executed MySQL DDL. Full-line SQL comments are removed before statement splitting, so punctuation in a comment cannot become executable SQL. Migration `008` adds forward-compatible access evidence columns and an index to the gateway-owned realtime snapshot table without changing its `(protocol, vin)` primary key. Migration `009` creates the per-group/topic/partition event-time checkpoint used to make MySQL effects authoritative before Kafka offsets are committed. Migration `012` creates the atomic, versioned business Scope projection; its reserved `row_number` column is quoted for production MySQL compatibility. Migration `014` backfills active vehicle-grant start times, creates the grant-interval history table and adds the active time lookup index; apply it before starting an API binary that writes grant history. Migration `015` adds platform-owned optimistic versions and immutable audits for per-vehicle location-source policy changes. It does not alter the gateway election SQL or expose the gateway `source_key`; the API resolves an opaque `sourceRef` server-side and the gateway applies the saved policy on the next valid vehicle report. Migration `016` adds customer name, department and responsible-person dimensions plus bounded lookup indexes to the platform-owned Scope projection. Migration `017` creates the reconciliation run, issue and immutable action tables; the unique fingerprint index is the database-level duplicate-work-item guard. +The API guards the access-threshold tables for compatibility, while alert and reconciliation APIs deliberately require their migrations to exist. Run every numbered migration explicitly before switching traffic so DDL permission and index creation failures are caught early. The migration journal records filename and SHA-256 and refuses a changed file; duplicate forward `ADD COLUMN` and `CREATE INDEX` statements are tolerated only when resuming partially executed MySQL DDL. Full-line SQL comments are removed before statement splitting, so punctuation in a comment cannot become executable SQL. Migration `008` adds forward-compatible access evidence columns and an index to the gateway-owned realtime snapshot table without changing its `(protocol, vin)` primary key. Migration `009` creates the per-group/topic/partition event-time checkpoint used to make MySQL effects authoritative before Kafka offsets are committed. Migration `012` creates the atomic, versioned business Scope projection; its reserved `row_number` column is quoted for production MySQL compatibility. Migration `014` backfills active vehicle-grant start times, creates the grant-interval history table and adds the active time lookup index; apply it before starting an API binary that writes grant history. Migration `015` adds platform-owned optimistic versions and immutable audits for per-vehicle location-source policy changes. It does not alter the gateway election SQL or expose the gateway `source_key`; the API resolves an opaque `sourceRef` server-side and the gateway applies the saved policy on the next valid vehicle report. Migration `016` adds customer name, department and responsible-person dimensions plus bounded lookup indexes to the platform-owned Scope projection. Migration `017` creates the reconciliation run, issue and immutable action tables; the unique fingerprint index is the database-level duplicate-work-item guard. Migration `018` records every OneOS-sourced brand fill before the platform identity OEM field changes; it does not grant access to OneOS or run a cross-database sync. + +`docs/oneos-brand-backfill.sql` is an explicit one-time calibration, not a service dependency. It reads OneOS vehicle/model master data, inserts immutable source evidence, and fills only empty `vehicle_identity_binding.oem` values. Review the pre-run count, apply migration `018`, run the script once through the migration runner or a transaction-capable MySQL client, and verify its final audit/missing counts. Never overwrite a non-empty platform brand and never schedule this script; future updates belong in the formal OneOS API. Production release `reconciliation-summary-fix-20260716191153` applied migration `017`, deployed the API, Web and reconciliation evaluator, and enabled the daily timer. The initial run detected 1,622 active differences in about 1.43 seconds; an immediate repeat completed in about 0.88 seconds with `new=0`. The authenticated smoke verified summary/list/detail for administrator and operator, viewer denial, optimistic-version conflict handling, evidence-key redaction, 50-row pagination, responsive desktop/mobile rendering and no browser console errors. +Production release `vehicle-count-authority-exact-20260716193428` made `vehicle_identity_binding` the authoritative denominator for vehicle coverage, coverage summaries, missing-source statistics and service readiness. Authenticated production smoke returned 1,024 vehicles from vehicle query, monitor, access management and source readiness, while keeping the 11 unbound realtime-only VINs as a separate `identityRequiredVehicles` operational metric and 11 `UNBOUND_SOURCE` reconciliation issues. Do not reintroduce the realtime snapshot union into formal vehicle totals; an unbound source becomes a vehicle only after the identity-binding workflow succeeds. + Release `oneos-api-client-20260716184601` applied migrations `012` and `016` and deployed the switchable API/database sync binary. The projection is intentionally empty (`active_version` unset) until a verified OneOS endpoint is configured. A production fail-closed smoke with `ONEOS_SCOPE_SOURCE=api` and no endpoint exited nonzero without publishing any row. Do not install or enable the timer until the endpoint, service token, signing secret, source-IP restriction and a representative snapshot have passed the contract checks below. Production release `source-diagnosis-stable-20260716173740` applied migration `015` before switching API traffic. The release gate verified 23 current assets and 42 compatibility assets. The authenticated diagnostic smoke used a real multi-source vehicle, confirmed that `source_key` was absent, and exercised the admin PUT route with values identical to the current policy; version, audit count and recommended source remained unchanged. Viewer/operator/admin access returned 403/200/200, median response time across 20 reads was approximately 70 ms (P95 79 ms), and the platform plus both alert evaluators remained active. diff --git a/vehicle-data-platform/docs/oneos-brand-backfill.sql b/vehicle-data-platform/docs/oneos-brand-backfill.sql new file mode 100644 index 00000000..1e71be45 --- /dev/null +++ b/vehicle-data-platform/docs/oneos-brand-backfill.sql @@ -0,0 +1,136 @@ +-- OneOS 车辆品牌一次性只读校准 +-- +-- 边界: +-- 1. 只读取 ln_asset_management.vehicle_info / vehicle_model。 +-- 2. 只更新中台 vehicle_identity_binding.oem 的空值,不覆盖已有品牌。 +-- 3. 每辆车在 vehicle_identity_oem_audit 留存来源编码、车型、版本和操作人。 +-- 4. 必须先应用 deploy/migrations/018_vehicle_oem_audit.sql。 +-- 5. 不得把本脚本改为定时跨库同步;后续持续同步应走 OneOS 正式 API。 + +SET SESSION MAX_EXECUTION_TIME = 10000; +SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; + +SELECT + COUNT(*) AS candidate_rows, + COUNT(DISTINCT b.vin) AS candidate_vehicles, + COUNT(DISTINCT CONCAT(b.vin, '|', LOWER(TRIM(vm.brand)), '|', COALESCE(TRIM(vm.model), ''))) AS distinct_source_values +FROM vehicle_identity_binding b +JOIN ln_asset_management.vehicle_info vi + ON BINARY vi.vin = BINARY b.vin + AND vi.del_flag = '0' +JOIN ln_asset_management.vehicle_model vm + ON vm.id = vi.vehicle_model_id + AND vm.del_flag = '0' +WHERE COALESCE(NULLIF(TRIM(b.oem), ''), '') = '' + AND COALESCE(NULLIF(TRIM(vm.brand), ''), '') <> ''; + +-- candidate_rows、candidate_vehicles、distinct_source_values 必须相等后才能继续。 +START TRANSACTION; + +INSERT INTO vehicle_identity_oem_audit ( + vin, + before_oem, + after_oem, + source_system, + source_version, + source_brand_code, + source_model_name, + source_updated_at, + actor +) +SELECT + b.vin, + COALESCE(b.oem, ''), + CASE LOWER(TRIM(vm.brand)) + WHEN 'hyundai' THEN + CASE + WHEN vm.model LIKE '%帕力安%' OR vm.model LIKE '%冷链%' OR vm.model LIKE '%双飞翼%' THEN '帕力安牌' + ELSE '现代' + END + WHEN 'yuejin' THEN '跃进' + WHEN 'feichi' THEN '飞驰' + WHEN 'sulong' THEN '苏龙' + WHEN 'higer' THEN '海格' + WHEN 'dongfeng' THEN '东风' + WHEN 'yutong' THEN '宇通' + WHEN 'chufeng' THEN '楚风' + WHEN 'tonghua' THEN '通华' + WHEN 'maxus' THEN '大通' + WHEN 'mingwei' THEN '明威' + WHEN 'wanfeng' THEN '万风' + WHEN 'shujie' THEN '舒捷' + WHEN 'denza' THEN '腾势' + WHEN 'hongyan' THEN '红岩' + WHEN 'yuanchang brand' THEN '远程牌' + WHEN 'others' THEN '其他' + ELSE TRIM(vm.brand) + END, + 'oneos_vehicle_model', + 'oneos-vehicle-model-20260716', + TRIM(vm.brand), + COALESCE(TRIM(vm.model), ''), + vm.update_time, + 'p1-02-master-data-calibration' +FROM vehicle_identity_binding b +JOIN ln_asset_management.vehicle_info vi + ON BINARY vi.vin = BINARY b.vin + AND vi.del_flag = '0' +JOIN ln_asset_management.vehicle_model vm + ON vm.id = vi.vehicle_model_id + AND vm.del_flag = '0' +WHERE COALESCE(NULLIF(TRIM(b.oem), ''), '') = '' + AND COALESCE(NULLIF(TRIM(vm.brand), ''), '') <> '' +ON DUPLICATE KEY UPDATE id = vehicle_identity_oem_audit.id; + +UPDATE vehicle_identity_binding b +JOIN ( + SELECT + vi.vin, + CASE LOWER(TRIM(vm.brand)) + WHEN 'hyundai' THEN + CASE + WHEN vm.model LIKE '%帕力安%' OR vm.model LIKE '%冷链%' OR vm.model LIKE '%双飞翼%' THEN '帕力安牌' + ELSE '现代' + END + WHEN 'yuejin' THEN '跃进' + WHEN 'feichi' THEN '飞驰' + WHEN 'sulong' THEN '苏龙' + WHEN 'higer' THEN '海格' + WHEN 'dongfeng' THEN '东风' + WHEN 'yutong' THEN '宇通' + WHEN 'chufeng' THEN '楚风' + WHEN 'tonghua' THEN '通华' + WHEN 'maxus' THEN '大通' + WHEN 'mingwei' THEN '明威' + WHEN 'wanfeng' THEN '万风' + WHEN 'shujie' THEN '舒捷' + WHEN 'denza' THEN '腾势' + WHEN 'hongyan' THEN '红岩' + WHEN 'yuanchang brand' THEN '远程牌' + WHEN 'others' THEN '其他' + ELSE TRIM(vm.brand) + END AS brand_name + FROM ln_asset_management.vehicle_info vi + JOIN ln_asset_management.vehicle_model vm + ON vm.id = vi.vehicle_model_id + AND vm.del_flag = '0' + WHERE vi.del_flag = '0' + AND COALESCE(NULLIF(TRIM(vi.vin), ''), '') <> '' + AND COALESCE(NULLIF(TRIM(vm.brand), ''), '') <> '' +) master + ON BINARY master.vin = BINARY b.vin +SET b.oem = master.brand_name +WHERE COALESCE(NULLIF(TRIM(b.oem), ''), '') = ''; + +COMMIT; + +SELECT + COUNT(*) AS audited_rows, + COUNT(DISTINCT vin) AS audited_vehicles +FROM vehicle_identity_oem_audit +WHERE source_system = 'oneos_vehicle_model' + AND source_version = 'oneos-vehicle-model-20260716'; + +SELECT COUNT(*) AS remaining_missing_oem +FROM vehicle_identity_binding +WHERE COALESCE(NULLIF(TRIM(oem), ''), '') = ''; diff --git a/vehicle-data-platform/docs/vehicle-data-platform-meeting-todo.md b/vehicle-data-platform/docs/vehicle-data-platform-meeting-todo.md index c2de0151..983c2cab 100644 --- a/vehicle-data-platform/docs/vehicle-data-platform-meeting-todo.md +++ b/vehicle-data-platform/docs/vehicle-data-platform-meeting-todo.md @@ -109,7 +109,7 @@ ### P0-02 全局监控客户列表重构 -状态:`待验收` +状态:`已完成` 已上线结果(release `monitor-customer-list-20260716160618`): @@ -119,6 +119,10 @@ - 后端显式返回位置、速度、SOC、总里程和今日里程可用性;JT808 不提供 SOC 时显示 `-`,不再把空值伪装为 `0%`。 - 地址仍由用户按需解析,坐标未变化时复用 6 小时前端缓存和 1 小时服务端缓存,并显示本次解析时间。 - ECS 真实数据预跑中,50 辆列表、296 辆无位置筛选、全量统计和全国地图分别约为 55ms、62ms、393ms 和 59ms。 +- 2026-07-16 最终复核时实时口径为 1,024 辆,其中 730 辆有位置、294 辆无位置;无位置筛选完整返回 294 辆。变化来自实时位置可用性,不改变主车辆总数。 +- JT808 单来源生产样本明确返回 `socAvailable=false`;抽样页的 `sourceCount` 与真实 `protocols` 数组全部一致,推荐来源均属于该车真实协议集合。 +- 生产桌面列表一次渲染 20 行,390×844 移动端一次挂载 20 个紧凑卡片;两种视图均无页面级横向溢出、告警、运行时异常或控制台错误。 +- 页面初始化和实时刷新不调用逆地理接口;生产浏览器网络检查为 0 次,只有用户点击“解析位置”后才请求。前端相同坐标缓存 6 小时,服务端 LRU 缓存 1 小时并合并同坐标并发请求。 目标: @@ -356,9 +360,25 @@ - 页面、详情和 CSV 统一改为“真实来源”口径;单一 JT808 在线且资料完整的车辆可判为正常,不再因不存在的 32960/宇通来源显示“接入不完整”。 - Go 全量测试、现行前端 49 个文件/254 项测试和生产构建通过。生产 50 辆列表 20 次请求中位约 48ms、P95 约 53ms,三个服务均 active。 +品牌资料二次校准: + +- 只读核对 OneOS `vehicle_info.vehicle_model_id → vehicle_model.brand/model` 后,确认原 252 辆缺品牌中有 244 辆具备唯一车型品牌,8 辆源系统本身仍无品牌。 +- 采用与 `ln-bi` 一致的品牌编码到展示名称口径,只填充中台 `vehicle_identity_binding.oem` 空值,不覆盖任何已有品牌;没有修改 `ln-asset-management`。 +- 迁移 `018_vehicle_oem_audit.sql` 建立独立品牌变更审计;一次性校准写入 244 行、244 个 VIN,`before_oem` 非空记录为 0,来源版本为 `oneos-vehicle-model-20260716`。 +- 生产接入页当前只剩 8 辆“品牌未维护”;资料待维护车辆从 356 降为 112。 + +车辆分母最终校准(release `vehicle-count-authority-exact-20260716193428`): + +- 车辆查询、覆盖汇总、缺失来源统计和运维服务汇总统一只以 `vehicle_identity_binding` 的 1,024 辆权威车辆为正式分母,不再把实时来源表中 11 个未绑定 VIN 提升为正式车辆。 +- 生产复核中,车辆查询、全局监控、接入管理、运维来源就绪度均返回 1,024 辆;运维汇总同时独立返回 `identityRequiredVehicles=11`,因此异常证据没有因分母修正而丢失。 +- 11 个未绑定来源仍保留在数据差异中心的 11 条 `UNBOUND_SOURCE` 待处理工单中;它们只能通过身份绑定流程进入正式车辆集,不能继续造成 1,035 辆的错误口径。 +- Go 全量测试、前端 49 个文件/259 项测试、TypeScript/Vite 生产构建、Web 资产烟测及四个 systemd 单元状态检查通过。 + 剩余业务资料依赖: -- 252 辆缺失品牌和 106 辆缺失 JT808 提供方需要业务/运维提供权威映射后补齐;会议中也确认当前品牌资料暂不准确。平台已形成完整待处理队列,但不会根据车牌、终端或位置猜测品牌。 +- 当前仍有 111 辆 JT808 提供方缺失。65 辆注册报文携带厂商码 `70504`,但生产中该编码同时对应 G7s、东方北斗和赛格,不能唯一映射;110 辆在 OneOS 只标记为“氢气智能管理平台”,这是聚合来源而非终端提供方;非当前来源中也没有额外 `source_code` 可补齐。 +- 因此这 111 辆继续留在待处理队列,需 GPS 运维/厂家提供 phone/终端到提供方的权威清单。平台不会根据终端号、IP、位置或厂商码猜测提供方。 +- 另有 8 辆 OneOS 车型主数据本身无品牌,需资产资料责任人补录后再通过正式 API 同步。 目标: