package platform import ( "context" "fmt" "net/url" "regexp" "strings" "time" ) var vehicleOperationStatuses = map[string]bool{ "unknown": true, "active": true, "inactive": true, "maintenance": true, "retired": true, } const ( vehicleProfileSyncLimit = 500 profileSyncCreated = "created" profileSyncUpdated = "updated" profileSyncUnchanged = "unchanged" profileSyncConflictSource = "conflict_source" profileSyncConflictSourceVersion = "conflict_source_version" profileSyncMissingVehicle = "missing_vehicle" profileSyncConflictPolicyPreserve = "preserve" profileSyncConflictPolicyOverwrite = "overwrite" ) var vehicleProfileSourceSystemPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._:-]*$`) func (s *Service) VehicleProfile(ctx context.Context, vin string) (VehicleProfile, error) { vin = strings.TrimSpace(vin) if err := authorizeVehicleVIN(ctx, vin); err != nil { return VehicleProfile{}, err } if err := validateProfileVIN(vin); err != nil { return VehicleProfile{}, err } store, ok := s.store.(VehicleProfileStore) if !ok { return decorateVehicleProfile(VehicleProfile{VIN: vin, OperationStatus: "unknown", SourceSystem: "unconfigured", MissingFields: []string{}}), nil } profile, exists, err := store.VehicleProfile(ctx, vin) if err != nil { return VehicleProfile{}, err } if !exists { profile = VehicleProfile{VIN: vin, OperationStatus: "unknown", SourceSystem: "unconfigured"} } return decorateVehicleProfile(profile), nil } func (s *Service) SaveVehicleProfile(ctx context.Context, vin string, input VehicleProfileInput) (VehicleProfile, error) { vin = strings.TrimSpace(vin) if err := validateProfileVIN(vin); err != nil { return VehicleProfile{}, err } store, ok := s.store.(VehicleProfileStore) if !ok { return VehicleProfile{}, fmt.Errorf("store does not provide durable vehicle profiles") } exists, err := s.vehicleExists(ctx, vin) if err != nil { return VehicleProfile{}, err } if !exists { return VehicleProfile{}, clientError{Code: "VEHICLE_NOT_FOUND", Message: "车辆身份不存在,不能创建主档"} } input = normalizeVehicleProfileInput(input) if err := validateVehicleProfileInput(input); err != nil { return VehicleProfile{}, err } profile, err := store.SaveVehicleProfile(ctx, vin, input) if err != nil { return VehicleProfile{}, err } return decorateVehicleProfile(profile), nil } func (s *Service) SyncVehicleProfiles(ctx context.Context, request VehicleProfileSyncRequest) (VehicleProfileSyncResult, error) { store, ok := s.store.(VehicleProfileStore) if !ok { return VehicleProfileSyncResult{}, fmt.Errorf("store does not provide durable vehicle profiles") } request = normalizeVehicleProfileSyncRequest(request) if err := validateVehicleProfileSyncRequest(request); err != nil { return VehicleProfileSyncResult{}, err } return store.SyncVehicleProfiles(ctx, request) } func normalizeVehicleProfileSyncRequest(request VehicleProfileSyncRequest) VehicleProfileSyncRequest { request.SourceSystem = strings.ToLower(strings.TrimSpace(request.SourceSystem)) request.SourceVersion = strings.TrimSpace(request.SourceVersion) request.ConflictPolicy = strings.ToLower(strings.TrimSpace(request.ConflictPolicy)) request.Actor = strings.TrimSpace(request.Actor) if request.ConflictPolicy == "" { request.ConflictPolicy = profileSyncConflictPolicyPreserve } for index := range request.Items { item := &request.Items[index] item.VIN = strings.ToUpper(strings.TrimSpace(item.VIN)) normalized := normalizeVehicleProfileInput(vehicleProfileSyncInput(*item, request.Actor)) item.ModelName = normalized.ModelName item.VehicleType = normalized.VehicleType item.CompanyName = normalized.CompanyName item.OperationStatus = normalized.OperationStatus item.AccessProvider = normalized.AccessProvider item.FirstAccessAt = normalized.FirstAccessAt if item.FirstAccessAt != "" { if parsed, err := parseVehicleProfileTime(item.FirstAccessAt); err == nil { item.FirstAccessAt = parsed.Format(time.RFC3339) } } } return request } func validateVehicleProfileSyncRequest(request VehicleProfileSyncRequest) error { if request.SourceSystem == "" || len(request.SourceSystem) > 64 || !vehicleProfileSourceSystemPattern.MatchString(request.SourceSystem) || request.SourceSystem == "manual" || request.SourceSystem == "unconfigured" { return clientError{Code: "VEHICLE_PROFILE_SYNC_SOURCE_INVALID", Message: "sourceSystem 必须是 64 字符内的外部系统标识"} } if request.SourceVersion == "" || len([]rune(request.SourceVersion)) > 128 { return clientError{Code: "VEHICLE_PROFILE_SYNC_VERSION_INVALID", Message: "sourceVersion 不能为空且不能超过 128 个字符"} } if request.ConflictPolicy != profileSyncConflictPolicyPreserve && request.ConflictPolicy != profileSyncConflictPolicyOverwrite { return clientError{Code: "VEHICLE_PROFILE_SYNC_POLICY_INVALID", Message: "conflictPolicy 仅支持 preserve 或 overwrite"} } if len(request.Items) == 0 || len(request.Items) > vehicleProfileSyncLimit { return clientError{Code: "VEHICLE_PROFILE_SYNC_SIZE_INVALID", Message: "单批车辆主档必须为 1 至 500 条"} } if request.Actor == "" { return clientError{Code: "VEHICLE_PROFILE_ACTOR_REQUIRED", Message: "缺少档案同步操作人"} } seen := make(map[string]struct{}, len(request.Items)) for _, item := range request.Items { if err := validateProfileVIN(item.VIN); err != nil { return err } if _, exists := seen[item.VIN]; exists { return clientError{Code: "VEHICLE_PROFILE_SYNC_DUPLICATE_VIN", Message: "同一批次不能包含重复 VIN"} } seen[item.VIN] = struct{}{} if err := validateVehicleProfileInput(vehicleProfileSyncInput(item, request.Actor)); err != nil { return err } } return nil } func vehicleProfileSyncInput(item VehicleProfileSyncItem, actor string) VehicleProfileInput { return VehicleProfileInput{ ModelName: item.ModelName, VehicleType: item.VehicleType, CompanyName: item.CompanyName, OperationStatus: item.OperationStatus, AccessProvider: item.AccessProvider, FirstAccessAt: item.FirstAccessAt, RuntimeSeconds: item.RuntimeSeconds, Actor: actor, } } func vehicleProfileSyncDecision(current VehicleProfile, exists bool, request VehicleProfileSyncRequest, item VehicleProfileSyncItem) string { if !exists { return profileSyncCreated } if current.SourceSystem == request.SourceSystem && current.SourceVersion == request.SourceVersion { if vehicleProfileSyncFieldsEqual(current, item) { return profileSyncUnchanged } return profileSyncConflictSourceVersion } if request.ConflictPolicy == profileSyncConflictPolicyPreserve && current.SourceSystem != request.SourceSystem { return profileSyncConflictSource } return profileSyncUpdated } func vehicleProfileSyncFieldsEqual(current VehicleProfile, item VehicleProfileSyncItem) bool { return current.ModelName == item.ModelName && current.VehicleType == item.VehicleType && current.CompanyName == item.CompanyName && current.OperationStatus == item.OperationStatus && current.AccessProvider == item.AccessProvider && current.FirstAccessAt == item.FirstAccessAt && equalOptionalInt64(current.RuntimeSeconds, item.RuntimeSeconds) } func equalOptionalInt64(left, right *int64) bool { if left == nil || right == nil { return left == nil && right == nil } return *left == *right } func countVehicleProfileSyncResult(result *VehicleProfileSyncResult, status string) { switch status { case profileSyncCreated: result.Created++ case profileSyncUpdated: result.Updated++ case profileSyncUnchanged: result.Unchanged++ case profileSyncMissingVehicle: result.Missing++ default: result.Conflicted++ } } func (s *Service) vehicleExists(ctx context.Context, vin string) (bool, error) { page, err := s.store.Vehicles(ctx, url.Values{"keyword": {vin}, "limit": {"20"}}) if err != nil { return false, err } for _, item := range page.Items { if strings.EqualFold(strings.TrimSpace(item.VIN), vin) { return true, nil } } return false, nil } func validateProfileVIN(vin string) error { if vin == "" || len(vin) > 32 { return clientError{Code: "VEHICLE_PROFILE_VIN_INVALID", Message: "VIN 不能为空且不能超过 32 个字符"} } return nil } func normalizeVehicleProfileInput(input VehicleProfileInput) VehicleProfileInput { input.ModelName = strings.TrimSpace(input.ModelName) input.VehicleType = strings.TrimSpace(input.VehicleType) input.CompanyName = strings.TrimSpace(input.CompanyName) input.OperationStatus = strings.ToLower(strings.TrimSpace(input.OperationStatus)) input.AccessProvider = strings.TrimSpace(input.AccessProvider) input.FirstAccessAt = strings.TrimSpace(input.FirstAccessAt) input.Actor = strings.TrimSpace(input.Actor) if input.OperationStatus == "" { input.OperationStatus = "unknown" } return input } func validateVehicleProfileInput(input VehicleProfileInput) error { lengths := []struct { value string max int name string }{ {input.ModelName, 128, "车型"}, {input.VehicleType, 64, "车辆类型"}, {input.CompanyName, 128, "所属企业"}, {input.AccessProvider, 128, "接入服务商"}, } for _, field := range lengths { if len([]rune(field.value)) > field.max { return clientError{Code: "VEHICLE_PROFILE_FIELD_INVALID", Message: field.name + "长度超出限制"} } } if !vehicleOperationStatuses[input.OperationStatus] { return clientError{Code: "VEHICLE_PROFILE_STATUS_INVALID", Message: "运营状态不在允许范围内"} } if input.RuntimeSeconds != nil && *input.RuntimeSeconds < 0 { return clientError{Code: "VEHICLE_PROFILE_RUNTIME_INVALID", Message: "累计运行时长不能为负数"} } if input.FirstAccessAt != "" { if _, err := parseVehicleProfileTime(input.FirstAccessAt); err != nil { return clientError{Code: "VEHICLE_PROFILE_TIME_INVALID", Message: "首次接入时间格式无效"} } } if input.Version < 0 { return clientError{Code: "VEHICLE_PROFILE_VERSION_INVALID", Message: "档案版本无效"} } if input.Actor == "" { return clientError{Code: "VEHICLE_PROFILE_ACTOR_REQUIRED", Message: "缺少档案维护人"} } return nil } func parseVehicleProfileTime(value string) (time.Time, error) { for _, layout := range []string{time.RFC3339, "2006-01-02T15:04", "2006-01-02 15:04:05"} { if parsed, err := time.ParseInLocation(layout, value, time.Local); err == nil { return parsed, nil } } return time.Time{}, fmt.Errorf("unsupported vehicle profile time") } func decorateVehicleProfile(profile VehicleProfile) VehicleProfile { missing := make([]string, 0, 7) if profile.ModelName == "" { missing = append(missing, "modelName") } if profile.VehicleType == "" { missing = append(missing, "vehicleType") } if profile.CompanyName == "" { missing = append(missing, "companyName") } if profile.OperationStatus == "" || profile.OperationStatus == "unknown" { missing = append(missing, "operationStatus") } if profile.AccessProvider == "" { missing = append(missing, "accessProvider") } if profile.FirstAccessAt == "" { missing = append(missing, "firstAccessAt") } if profile.RuntimeSeconds == nil { missing = append(missing, "runtimeSeconds") } profile.MissingFields = missing profile.Completeness = (7 - len(missing)) * 100 / 7 if profile.OperationStatus == "" { profile.OperationStatus = "unknown" } if profile.SourceSystem == "" { profile.SourceSystem = "unconfigured" } return profile }