134 lines
4.4 KiB
Go
134 lines
4.4 KiB
Go
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, "实名观测来源无效")
|
|
}
|
|
}
|