Files
junhong_cmp_fiber/internal/domain/cardobservation/realname.go
2026-07-24 16:07:18 +08:00

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