package cardobservation import ( "strings" "time" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) const ( realnameReversalThreshold = 3 realnameReversalWindow = 10 * time.Minute ) // ObservationMetadata 描述一次上游实名观测的可追踪元数据。 type ObservationMetadata struct { ObservationID string Source string Scene string ObservedAt time.Time RequestID string CorrelationID string UpstreamSummary string } // RealnameObservation 是领域层接收的标准实名观测。 type RealnameObservation struct { CardID uint Verified bool Metadata ObservationMetadata } // CardRealnameSnapshot 是应用层从持久化模型映射出的最小卡实名快照。 type CardRealnameSnapshot struct { CardID uint Status int FirstRealnameAt *time.Time ReversalCount int ReversalStartedAt *time.Time } // RealnameDecision 描述应用本次应持久化的实名事实。 type RealnameDecision struct { StatusChanged bool FirstVerified bool ReversalPending bool ReversalCount int ReversalStartedAt *time.Time ReversalReset bool BeforeStatus int AfterStatus int } // ApplyRealname 根据观测来源应用首次实名和周期逆转规则。 func ApplyRealname(snapshot CardRealnameSnapshot, observation RealnameObservation) (RealnameDecision, error) { if err := validateObservation(observation); err != nil { return RealnameDecision{}, err } decision := RealnameDecision{ BeforeStatus: snapshot.Status, AfterStatus: snapshot.Status, ReversalCount: snapshot.ReversalCount, ReversalStartedAt: snapshot.ReversalStartedAt, } if snapshot.Status != constants.RealNameStatusNotVerified && snapshot.Status != constants.RealNameStatusVerified { return RealnameDecision{}, errors.New(errors.CodeInvalidStatus, "卡实名状态无效") } if observation.Verified { decision.AfterStatus = constants.RealNameStatusVerified decision.StatusChanged = snapshot.Status != decision.AfterStatus decision.FirstVerified = decision.StatusChanged && snapshot.FirstRealnameAt == nil decision.ReversalReset = snapshot.ReversalCount != 0 || snapshot.ReversalStartedAt != nil decision.ReversalCount = 0 decision.ReversalStartedAt = nil return decision, nil } if snapshot.Status == constants.RealNameStatusNotVerified { decision.ReversalReset = snapshot.ReversalCount != 0 || snapshot.ReversalStartedAt != nil decision.ReversalCount = 0 decision.ReversalStartedAt = nil return decision, nil } if observation.Metadata.Source == constants.CardObservationSourceCarrierCallback { // 解除实名回调只留痕,不把外部单次结果变成本地逆转事实。 return decision, nil } if observation.Metadata.Source == constants.CardObservationSourceManualOverride { decision.AfterStatus = constants.RealNameStatusNotVerified decision.StatusChanged = true decision.ReversalReset = true decision.ReversalCount = 0 decision.ReversalStartedAt = nil return decision, nil } count := snapshot.ReversalCount startedAt := snapshot.ReversalStartedAt now := observation.Metadata.ObservedAt if startedAt == nil || now.Before(*startedAt) || now.Sub(*startedAt) > realnameReversalWindow { count = 0 startedAt = &now } count++ decision.ReversalCount = count decision.ReversalStartedAt = startedAt if count < realnameReversalThreshold { decision.ReversalPending = true return decision, nil } decision.AfterStatus = constants.RealNameStatusNotVerified decision.StatusChanged = true decision.ReversalReset = true decision.ReversalCount = 0 decision.ReversalStartedAt = nil return decision, nil } func validateObservation(observation RealnameObservation) error { if observation.CardID == 0 || strings.TrimSpace(observation.Metadata.ObservationID) == "" || strings.TrimSpace(observation.Metadata.Source) == "" || strings.TrimSpace(observation.Metadata.Scene) == "" || observation.Metadata.ObservedAt.IsZero() { return errors.New(errors.CodeInvalidParam, "实名观测关键字段缺失") } switch observation.Metadata.Source { case constants.CardObservationSourcePolling, constants.CardObservationSourceManualSync, constants.CardObservationSourceManualOverride, constants.CardObservationSourceCarrierCallback, constants.CardObservationSourceBusinessEvent: return nil default: return errors.New(errors.CodeInvalidParam, "实名观测来源无效") } }