package platform import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "strings" ) type vehicleSourcePolicyStore interface { VehicleSourcePolicy(context.Context, string) (VehicleSourcePolicyConfig, error) SaveVehicleSourcePolicy(context.Context, vehicleSourcePolicyStoreUpdate) (VehicleSourcePolicyConfig, error) VehicleLocationSourceHistory(context.Context, string) ([]vehicleLocationSourceHistory, error) VehicleSourceProvider(context.Context, string, string, string) (string, error) } func sourceReference(vin, protocol, sourceKey string) string { sum := sha256.Sum256([]byte(strings.TrimSpace(vin) + "\x00" + strings.TrimSpace(protocol) + "\x00" + sourceKey)) return hex.EncodeToString(sum[:]) } func (s *Service) VehicleSourceDiagnostic(ctx context.Context, vin string) (VehicleSourceDiagnostic, error) { if err := authorizeInternalOperations(ctx, false); err != nil { return VehicleSourceDiagnostic{}, err } evidence, err := s.VehicleSourceEvidence(ctx, vin, "") if err != nil { return VehicleSourceDiagnostic{}, err } store, ok := s.store.(vehicleSourcePolicyStore) if !ok { return VehicleSourceDiagnostic{}, errors.New("vehicle source policy store is not configured") } history, err := store.VehicleLocationSourceHistory(ctx, evidence.VIN) if err != nil { return VehicleSourceDiagnostic{}, err } historyBySource := make(map[string]vehicleLocationSourceHistory, len(history)) for _, item := range history { historyBySource[item.Protocol+"\x00"+item.sourceKey] = item } var access *AccessVehicleRow page, accessErr := s.AccessVehicles(ctx, AccessQuery{Keyword: evidence.VIN, Limit: 10}) if accessErr == nil { for index := range page.Items { if page.Items[index].VIN == evidence.VIN { row := page.Items[index] access = &row break } } } protocolStatus := map[string]AccessProtocolStatus{} if access != nil { for _, status := range access.ProtocolStatuses { protocolStatus[status.Protocol] = status } } representedProtocols := make(map[string]bool, len(evidence.LocationSources)) for _, source := range evidence.LocationSources { representedProtocols[source.Protocol] = true } if access != nil { for _, status := range access.ProtocolStatuses { if !status.Connected || representedProtocols[status.Protocol] { continue } sourceKey := status.Protocol + ":canonical" providerOverride, providerErr := store.VehicleSourceProvider(ctx, evidence.VIN, status.Protocol, sourceKey) if providerErr != nil { return VehicleSourceDiagnostic{}, providerErr } source := VehicleLocationSourceEvidence{ Protocol: status.Protocol, SourceLabel: firstNonEmpty(providerOverride, status.Provider, status.Protocol), ProviderOverride: providerOverride, SourceKind: "CANONICAL", SelectedWithinProtocol: true, Recommended: evidence.RecommendedLocationProtocol == status.Protocol, Enabled: true, Priority: 100, Online: status.OnlineState == "online", QualityStatus: "NO_LOCATION", QualityReason: "协议已形成接入快照,但当前没有可展开的独立位置来源", EventTime: status.LatestEventAt, ReceivedAt: status.LatestReceivedAt, FirstSeenAt: status.FirstSeenAt, ReportIntervalSec: status.ReportIntervalSec, sourceKey: sourceKey, } evidence.LocationSources = append(evidence.LocationSources, source) representedProtocols[status.Protocol] = true if source.Recommended { evidence.RecommendedLocationLabel = source.SourceLabel } } } for index := range evidence.LocationSources { source := &evidence.LocationSources[index] if source.sourceKey != "" { source.SourceRef = sourceReference(evidence.VIN, source.Protocol, source.sourceKey) } if item, exists := historyBySource[source.Protocol+"\x00"+source.sourceKey]; exists { source.FirstSeenAt = item.FirstSeenAt source.ReportSampleCount = item.ReportSampleCount } if status, exists := protocolStatus[source.Protocol]; exists { source.ReportIntervalSec = status.ReportIntervalSec if source.FirstSeenAt == "" { source.FirstSeenAt = status.FirstSeenAt } } source.SelectionReason = sourceSelectionReason(*source) } policy, err := store.VehicleSourcePolicy(ctx, evidence.VIN) if err != nil { return VehicleSourceDiagnostic{}, err } return VehicleSourceDiagnostic{ Evidence: evidence, Access: access, Policy: policy, RecommendationReason: recommendationReason(evidence), RefreshHint: "策略保存后由车辆下一次有效上报触发网关重新选举;页面不会伪造即时切换结果。", }, nil } func (s *Service) UpdateVehicleSourcePolicy(ctx context.Context, vin string, update VehicleSourcePolicyUpdate) (VehicleSourceDiagnostic, error) { if err := authorizeInternalOperations(ctx, true); err != nil { return VehicleSourceDiagnostic{}, err } vin = strings.ToUpper(strings.TrimSpace(vin)) update.SourceRef = strings.ToLower(strings.TrimSpace(update.SourceRef)) update.ProviderName = strings.TrimSpace(update.ProviderName) update.ProviderEvidence = strings.TrimSpace(update.ProviderEvidence) update.Remark = strings.TrimSpace(update.Remark) if vin == "" { return VehicleSourceDiagnostic{}, clientError{Code: "VEHICLE_VIN_REQUIRED", Message: "车辆 VIN 不能为空"} } if update.Version < 1 { return VehicleSourceDiagnostic{}, clientError{Code: "SOURCE_POLICY_VERSION_INVALID", Message: "来源策略版本无效,请刷新后重试"} } if len(update.SourceRef) != sha256.Size*2 { return VehicleSourceDiagnostic{}, clientError{Code: "SOURCE_REF_INVALID", Message: "来源引用无效"} } if _, err := hex.DecodeString(update.SourceRef); err != nil { return VehicleSourceDiagnostic{}, clientError{Code: "SOURCE_REF_INVALID", Message: "来源引用无效"} } if update.Priority < 1 || update.Priority > 1000 { return VehicleSourceDiagnostic{}, clientError{Code: "SOURCE_PRIORITY_INVALID", Message: "来源优先级必须在 1 到 1000 之间"} } if len([]rune(update.Remark)) > 200 { return VehicleSourceDiagnostic{}, clientError{Code: "SOURCE_REMARK_TOO_LONG", Message: "策略备注不能超过 200 个字符"} } if len([]rune(update.ProviderName)) > 128 { return VehicleSourceDiagnostic{}, clientError{Code: "SOURCE_PROVIDER_TOO_LONG", Message: "提供方名称不能超过 128 个字符"} } if len([]rune(update.ProviderEvidence)) > 255 { return VehicleSourceDiagnostic{}, clientError{Code: "SOURCE_PROVIDER_EVIDENCE_TOO_LONG", Message: "提供方核验依据不能超过 255 个字符"} } update.Actor = strings.TrimSpace(update.Actor) if update.Actor == "" { update.Actor = ActorFromContext(ctx) } evidenceStore, ok := s.store.(VehicleSourceEvidenceStore) if !ok { return VehicleSourceDiagnostic{}, errors.New("vehicle source evidence store is not configured") } evidence, err := evidenceStore.VehicleSourceEvidence(ctx, vin, "") if err != nil { return VehicleSourceDiagnostic{}, err } var selected *VehicleLocationSourceEvidence for index := range evidence.LocationSources { source := &evidence.LocationSources[index] if source.sourceKey != "" && sourceReference(vin, source.Protocol, source.sourceKey) == update.SourceRef { selected = source break } } if selected == nil || selected.sourceKey == "" { return VehicleSourceDiagnostic{}, clientError{Code: "SOURCE_NOT_FOUND", Message: "当前车辆不存在该来源,请刷新后重试"} } policyConfigurable := !strings.EqualFold(selected.SourceKind, "CANONICAL") if !policyConfigurable && (selected.Enabled != update.Enabled || selected.Priority != update.Priority || strings.TrimSpace(selected.PolicyRemark) != update.Remark) { return VehicleSourceDiagnostic{}, clientError{ Code: "SOURCE_POLICY_CANONICAL_READ_ONLY", Message: "协议融合快照只能维护提供方,不能调整启停、优先级或策略备注", } } providerChanged := strings.TrimSpace(selected.ProviderOverride) != update.ProviderName if providerChanged && update.ProviderEvidence == "" { return VehicleSourceDiagnostic{}, clientError{Code: "SOURCE_PROVIDER_REASON_REQUIRED", Message: "维护提供方时必须填写权威资料来源或核验说明"} } store, ok := s.store.(vehicleSourcePolicyStore) if !ok { return VehicleSourceDiagnostic{}, errors.New("vehicle source policy store is not configured") } if _, err := store.SaveVehicleSourcePolicy(ctx, vehicleSourcePolicyStoreUpdate{ VehicleSourcePolicyUpdate: update, VIN: vin, Protocol: selected.Protocol, SourceKey: selected.sourceKey, SourceLabel: selected.SourceLabel, CurrentEnabled: selected.Enabled, CurrentPriority: selected.Priority, CurrentRemark: selected.PolicyRemark, CurrentProvider: selected.ProviderOverride, PolicyConfigurable: policyConfigurable, }); err != nil { return VehicleSourceDiagnostic{}, err } return s.VehicleSourceDiagnostic(ctx, vin) } func authorizeInternalOperations(ctx context.Context, adminOnly bool) error { principal, ok := PrincipalFromContext(ctx) if !ok { return nil } if principal.UserType == "customer" || principal.Role == "customer" { return clientError{Code: "PERMISSION_DENIED", Message: "客户账号无权访问内部来源策略"} } if adminOnly && principal.Role != "admin" { return clientError{Code: "PERMISSION_DENIED", Message: "只有管理员可以修改来源策略"} } return nil } func sourceSelectionReason(source VehicleLocationSourceEvidence) string { if strings.EqualFold(source.SourceKind, "CANONICAL") { return "这是协议融合结果快照,不是独立终端候选;可维护经权威资料确认的提供方,但不能从该快照调整终端启停或优先级。" } if !source.Enabled { return "已由运维策略停用,不参与协议内选举。" } if source.QualityStatus != "" && !strings.EqualFold(source.QualityStatus, "OK") { return fmt.Sprintf("质量状态为 %s,当前不应作为首选;%s", source.QualityStatus, firstNonEmpty(source.QualityReason, "请核对原始证据")) } if source.Recommended { return fmt.Sprintf("当前融合推荐;协议内已选中,策略优先级 %d,并通过在线与质量检查。", source.Priority) } if source.SelectedWithinProtocol { return fmt.Sprintf("当前为 %s 协议内候选,策略优先级 %d;跨协议融合当前推荐其他来源。", source.Protocol, source.Priority) } if !source.Online { return fmt.Sprintf("来源当前离线,策略优先级 %d;在线来源会优先参与选举。", source.Priority) } return fmt.Sprintf("来源可用,策略优先级 %d;当前候选在新鲜度、连续有效样本或稳定性排序中未胜出。", source.Priority) } func recommendationReason(evidence VehicleSourceEvidence) string { for _, source := range evidence.LocationSources { if source.Recommended { if strings.EqualFold(source.SourceKind, "CANONICAL") { return fmt.Sprintf("当前推荐 %s 的协议融合结果;本次证据未取得独立终端候选,只能维护权威提供方,不能从该快照反推人工优先级。", source.Protocol) } return fmt.Sprintf( "当前推荐 %s / %s:来源已启用、质量为 %s、协议内选中,策略优先级为 %d。选举还会综合两分钟新鲜度、漂移冲突保护和连续有效样本。", source.Protocol, source.SourceLabel, firstNonEmpty(source.QualityStatus, "未知"), source.Priority, ) } } if len(evidence.LocationSources) == 0 { return "当前车辆没有可用于位置选举的来源证据。" } return "当前没有明确推荐来源;请检查来源启用状态、质量、新鲜度和网关下一次有效上报。" }