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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user