feat: sync vehicle monitoring and accumulated platform updates

Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates.

Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
This commit is contained in:
lingniu
2026-10-26 18:05:59 +08:00
parent 8b32ae2988
commit d6b957dfc8
290 changed files with 28238 additions and 1647 deletions
@@ -549,21 +549,22 @@ type monitorActivityStore interface {
}
func (s *Service) buildMonitorSummary(ctx context.Context, query url.Values, vehicles Page[VehicleRealtimeRow]) (MonitorSummary, error) {
var dashboard DashboardSummary
var err error
if store, ok := s.store.(monitorActivityStore); ok {
dashboard, err = store.MonitorActivitySummary(ctx)
} else {
dashboard, err = s.store.DashboardSummary(ctx)
}
if err != nil {
return MonitorSummary{}, err
}
result := MonitorSummary{
ActiveToday: dashboard.ActiveToday,
FrameToday: dashboard.FrameToday,
Truncated: vehicles.Total > len(vehicles.Items),
AsOf: time.Now().UTC().Format(time.RFC3339),
result := MonitorSummary{Truncated: vehicles.Total > len(vehicles.Items), AsOf: time.Now().UTC().Format(time.RFC3339)}
principal, scoped := PrincipalFromContext(ctx)
if !scoped || !principal.HasVehicleScope() {
var dashboard DashboardSummary
var err error
if store, ok := s.store.(monitorActivityStore); ok {
dashboard, err = store.MonitorActivitySummary(ctx)
} else {
dashboard, err = s.store.DashboardSummary(ctx)
}
if err != nil {
return MonitorSummary{}, err
}
result.ActiveToday = dashboard.ActiveToday
result.FrameToday = dashboard.FrameToday
result.ActivityDataAvailable = true
}
activeAlertVINs := map[string]struct{}{}
if store, ok := s.store.(monitorAlertStore); ok {
@@ -860,6 +861,15 @@ func (s *Service) SourceReadiness(ctx context.Context) (SourceReadinessPlan, err
}
func (s *Service) VehicleRealtime(ctx context.Context, query url.Values) (Page[VehicleRealtimeRow], error) {
// A partial fleet number may also match a plate/phone. Do not resolve that
// search to the first vehicle before the list's OR predicate can run.
if strings.TrimSpace(query.Get("vin")) == "" && len(vehicleNumberVINs(vehicleSearchKeywords(query), false)) > 0 {
scopedQuery, err := applyPrincipalVehicleScope(ctx, query)
if err != nil {
return Page[VehicleRealtimeRow]{}, err
}
return s.store.VehicleRealtime(ctx, scopedQuery)
}
resolvedQuery, err := s.resolveVehicleQuery(ctx, query)
if err != nil {
return Page[VehicleRealtimeRow]{}, err
@@ -870,7 +880,7 @@ func (s *Service) VehicleRealtime(ctx context.Context, query url.Values) (Page[V
func (s *Service) VehicleServiceOverview(ctx context.Context, keyword string, protocol string) (VehicleServiceOverview, error) {
keyword = strings.TrimSpace(keyword)
protocol = strings.TrimSpace(protocol)
if principal, ok := PrincipalFromContext(ctx); ok && principal.UserType == "customer" {
if principal, ok := PrincipalFromContext(ctx); ok && principal.HasVehicleScope() {
resolvedScopeVIN, err := s.resolveVehicleVIN(ctx, keyword, protocol)
if err != nil {
return VehicleServiceOverview{}, err
@@ -965,7 +975,7 @@ func (s *Service) VehicleServiceOverviews(ctx context.Context, query VehicleOver
func principalVehicleVINScope(ctx context.Context) []string {
principal, ok := PrincipalFromContext(ctx)
if !ok || principal.UserType != "customer" {
if !ok || !principal.HasVehicleScope() {
return nil
}
scope := make([]string, 0, len(principal.VehicleVINs))
@@ -979,7 +989,7 @@ func principalVehicleVINScope(ctx context.Context) []string {
func restrictVehicleOverviewPage(ctx context.Context, page Page[VehicleServiceOverview]) Page[VehicleServiceOverview] {
principal, ok := PrincipalFromContext(ctx)
if !ok || principal.UserType != "customer" {
if !ok || !principal.HasVehicleScope() {
return page
}
for index := range page.Items {
@@ -992,6 +1002,17 @@ func restrictVehicleOverviewPage(ctx context.Context, page Page[VehicleServiceOv
}
func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string) (VehicleDetail, error) {
return s.vehicleDetail(ctx, vin, protocol, true)
}
// VehicleLiveDetail loads the single-vehicle workspace without historical
// previews. Those previews belong to the history pages and must not make live
// identity, location, profile and business data depend on historical storage.
func (s *Service) VehicleLiveDetail(ctx context.Context, vin string, protocol string) (VehicleDetail, error) {
return s.vehicleDetail(ctx, vin, protocol, false)
}
func (s *Service) vehicleDetail(ctx context.Context, vin string, protocol string, includeHistory bool) (VehicleDetail, error) {
keyword := strings.TrimSpace(vin)
protocol = strings.TrimSpace(protocol)
vehicleQuery := url.Values{"keyword": {keyword}, "limit": {"10"}}
@@ -1028,6 +1049,7 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
return VehicleDetail{}, err
}
return VehicleDetail{
HistoryIncluded: includeHistory,
VIN: "",
LookupKey: keyword,
LookupResolved: false,
@@ -1080,24 +1102,31 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
if err != nil {
return VehicleDetail{}, err
}
history, err := s.store.HistoryLocationsFromTDengine(ctx, historyQuery)
if err != nil {
return VehicleDetail{}, err
}
raw, err := s.vehicleDetailRawPreview(ctx, rawQuery)
if err != nil {
return VehicleDetail{}, err
}
mileage, err := s.store.DailyMileage(ctx, mileageQuery)
if err != nil {
return VehicleDetail{}, err
history := Page[HistoryLocationRow]{Items: []HistoryLocationRow{}, Limit: 20}
raw := Page[RawFrameRow]{Items: []RawFrameRow{}, Limit: 10}
mileage := Page[DailyMileageRow]{Items: []DailyMileageRow{}, Limit: 20}
if includeHistory {
history, err = s.store.HistoryLocationsFromTDengine(ctx, historyQuery)
if err != nil {
return VehicleDetail{}, err
}
raw, err = s.vehicleDetailRawPreview(ctx, rawQuery)
if err != nil {
return VehicleDetail{}, err
}
mileage, err = s.store.DailyMileage(ctx, mileageQuery)
if err != nil {
return VehicleDetail{}, err
}
}
quality, err := s.store.QualityIssues(ctx, qualityQuery)
if err != nil {
return VehicleDetail{}, err
}
observedSourceStatus := vehicleSourceStatus(vehicles.Items, realtime.Items, history.Items, raw.Items, mileage.Items)
observedSourceStatus = s.enrichVehicleSourceStatus(ctx, queryVIN, observedSourceStatus)
if includeHistory {
observedSourceStatus = s.enrichVehicleSourceStatus(ctx, queryVIN, observedSourceStatus)
}
availableSources := sourceNamesForProtocol(observedSourceStatus, protocol)
sourceStatus := completeVehicleSourceStatus(observedSourceStatus, protocol)
serviceStatus := buildVehicleServiceStatus(true, sourceStatus)
@@ -1125,6 +1154,7 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
}
}
return VehicleDetail{
HistoryIncluded: includeHistory,
VIN: resolvedVIN,
LookupKey: keyword,
LookupResolved: resolvedVIN != "",
@@ -1150,10 +1180,11 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
}
const vehicleDetailRawProjectionLookback = 24 * time.Hour
const vehicleDetailRawPreviewTimeout = time.Second
type vehicleDetailRawPreviewResult struct {
anchor RawFrameRow
page Page[RawFrameRow]
index int
page Page[RawFrameRow]
}
// vehicleDetailRawPreview preserves the latest-ever semantics without issuing
@@ -1187,32 +1218,47 @@ func (s *Service) vehicleDetailRawPreview(ctx context.Context, query RawFrameQue
return Page[RawFrameRow]{Items: []RawFrameRow{}, Total: 0, Limit: limit, Offset: 0}, nil
}
previewCtx, cancel := context.WithTimeout(ctx, vehicleDetailRawPreviewTimeout)
defer cancel()
results := make(chan vehicleDetailRawPreviewResult, len(anchors.Items))
for _, anchor := range anchors.Items {
anchor := anchor
previews := make([][]RawFrameRow, len(anchors.Items))
for index, anchor := range anchors.Items {
previews[index] = []RawFrameRow{anchor}
go func() {
bounded, valid := boundedRawPreviewQuery(query, anchor)
if !valid {
results <- vehicleDetailRawPreviewResult{anchor: anchor}
results <- vehicleDetailRawPreviewResult{index: index}
return
}
page, queryErr := s.store.RawFrames(ctx, bounded)
page, queryErr := s.store.RawFrames(previewCtx, bounded)
if queryErr != nil {
results <- vehicleDetailRawPreviewResult{anchor: anchor}
results <- vehicleDetailRawPreviewResult{index: index}
return
}
results <- vehicleDetailRawPreviewResult{anchor: anchor, page: page}
results <- vehicleDetailRawPreviewResult{index: index, page: page}
}()
}
items := make([]RawFrameRow, 0, len(anchors.Items)*query.Limit)
// Some historical drivers return after their context expires. Keep a
// buffered result channel and select on the budget so a slow driver cannot
// prevent the already-read durable projections from reaching the caller.
collect:
for range anchors.Items {
result := <-results
if len(result.page.Items) == 0 {
items = append(items, result.anchor)
continue
select {
case result := <-results:
if len(result.page.Items) > 0 {
previews[result.index] = result.page.Items
}
case <-previewCtx.Done():
if ctx.Err() != nil {
return Page[RawFrameRow]{}, ctx.Err()
}
break collect
}
items = append(items, result.page.Items...)
}
items := make([]RawFrameRow, 0, len(anchors.Items))
for _, preview := range previews {
items = append(items, preview...)
}
items = newestRawFrames(items, query.Limit)
limit := query.Limit
@@ -1361,7 +1407,7 @@ func (s *Service) VehicleSourceEvidence(ctx context.Context, vin string, date st
func authorizeVehicleDailyEvidenceDate(ctx context.Context, vin string, date string) error {
principal, ok := PrincipalFromContext(ctx)
if !ok || principal.UserType != "customer" {
if !ok || !principal.HasHistoryTimeScope() {
return nil
}
grant, ok := principal.VehicleGrant(vin)
@@ -4287,7 +4333,14 @@ func canAccessHistoryExport(principal Principal, job *HistoryExportJob) bool {
if job == nil {
return false
}
if principal.Role == "admin" || principal.UserType == "admin" {
if principal.HasVehicleScope() {
for _, vin := range job.VehicleVINs {
if !principal.CanVIN(vin) {
return false
}
}
}
if (principal.Role == "admin" || principal.UserType == "admin") && !principal.HasVehicleScope() {
return true
}
ownerID := historyExportOwnerID(principal)
@@ -5841,11 +5894,19 @@ func offlineDurationMinutes(online bool, lastSeen string, now time.Time) *int {
}
func (s *Service) QualitySummary(ctx context.Context, query url.Values) (QualitySummary, error) {
return s.store.QualitySummary(ctx, query)
q, err := applyPrincipalVehicleScope(ctx, query)
if err != nil {
return QualitySummary{}, err
}
return s.store.QualitySummary(ctx, q)
}
func (s *Service) QualityIssues(ctx context.Context, query url.Values) (Page[QualityIssueRow], error) {
return s.store.QualityIssues(ctx, query)
q, err := applyPrincipalVehicleScope(ctx, query)
if err != nil {
return Page[QualityIssueRow]{}, err
}
return s.store.QualityIssues(ctx, q)
}
func (s *Service) QualityNotificationPlan(ctx context.Context, query url.Values) (QualityNotificationPlan, error) {
@@ -6127,7 +6188,7 @@ func (s *Service) resolveVehicleQuery(ctx context.Context, query url.Values) (ur
return nil, err
}
if resolvedVIN != "" {
if principal, ok := PrincipalFromContext(ctx); ok && principal.UserType == "customer" && !principal.CanVIN(resolvedVIN) {
if principal, ok := PrincipalFromContext(ctx); ok && principal.HasVehicleScope() && !principal.CanVIN(resolvedVIN) {
return nil, clientError{Code: "VEHICLE_PERMISSION_DENIED", Message: "当前账号无权查看该车辆"}
}
resolved.Set("vin", resolvedVIN)
@@ -6162,11 +6223,16 @@ func (s *Service) resolveVehicleVIN(ctx context.Context, keyword string, protoco
func applyPrincipalVehicleScope(ctx context.Context, query url.Values) (url.Values, error) {
next := cloneValues(query)
principal, ok := PrincipalFromContext(ctx)
if !ok || principal.UserType != "customer" {
if !ok || !principal.HasVehicleScope() {
return next, nil
}
allowed := make(map[string]bool, len(principal.VehicleVINs))
activeVINs := make([]string, 0, len(principal.VehicleVINs))
for _, vin := range principal.VehicleVINs {
if !principal.CanVIN(vin) {
continue
}
activeVINs = append(activeVINs, vin)
allowed[strings.ToUpper(strings.TrimSpace(vin))] = true
}
for _, value := range []string{next.Get("vin"), next.Get("vins")} {
@@ -6177,10 +6243,14 @@ func applyPrincipalVehicleScope(ctx context.Context, query url.Values) (url.Valu
}
}
}
if len(principal.VehicleVINs) == 0 {
if len(activeVINs) == 0 {
next.Set("scopeVins", "__NO_VEHICLE_SCOPE__")
} else {
next.Set("scopeVins", strings.Join(principal.VehicleVINs, ","))
next.Set("scopeVins", strings.Join(activeVINs, ","))
}
if !principal.HasHistoryTimeScope() {
next.Del("scopeVehicleFrom")
return next, nil
}
validFromByVIN := make(map[string]string, len(principal.VehicleGrants))
shanghai := time.FixedZone("Asia/Shanghai", 8*60*60)
@@ -6205,9 +6275,12 @@ func applyPrincipalVehicleScope(ctx context.Context, query url.Values) (url.Valu
}
func applyPrincipalHistoryTimeScope(ctx context.Context, vin string, query url.Values) (url.Values, error) {
if err := authorizeVehicleVIN(ctx, vin); err != nil {
return nil, err
}
next := cloneValues(query)
principal, ok := PrincipalFromContext(ctx)
if !ok || principal.UserType != "customer" {
if !ok || !principal.HasHistoryTimeScope() {
return next, nil
}
grant, ok := principal.VehicleGrant(vin)
@@ -6251,7 +6324,7 @@ func applyPrincipalHistoryTimeScope(ctx context.Context, vin string, query url.V
func requirePrincipalHistoricalGrantScope(ctx context.Context, query url.Values) error {
principal, ok := PrincipalFromContext(ctx)
if !ok || principal.UserType != "customer" {
if !ok || !principal.HasHistoryTimeScope() {
return nil
}
requested := make([]string, 0)
@@ -6277,7 +6350,7 @@ func requirePrincipalHistoricalGrantScope(ctx context.Context, query url.Values)
func authorizeVehicleVIN(ctx context.Context, vin string) error {
principal, ok := PrincipalFromContext(ctx)
if !ok || principal.UserType != "customer" {
if !ok || !principal.HasVehicleScope() {
return nil
}
vin = strings.ToUpper(strings.TrimSpace(vin))