package task import ( "context" "encoding/json" "time" carddomain "github.com/break/junhong_cmp_fiber/internal/domain/cardobservation" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // 入库消息的观测类型。 const ( ingestKindTraffic = "traffic" ingestKindNetwork = "network" ingestKindRealname = "realname" ) // ingestRawResponseLimit 是入库消息携带原始响应的上限:比采集记录保存上限多 1 字节,使"超过 4KB 截断"判断不变,同时控制消息大小。 const ingestRawResponseLimit = 4096 + 1 // IngestMessage 是一次轮询观测的入库消息(topic card.ingest)。采集时间取网关返回时刻,入库全程按它归属日期与判断新旧。 type IngestMessage struct { Kind string `json:"kind"` CardID uint `json:"card_id"` CarrierID uint `json:"carrier_id"` ICCID string `json:"iccid"` ObservationID string `json:"observation_id"` ObservedAt time.Time `json:"observed_at"` Source string `json:"source"` Scene string `json:"scene"` CorrelationID string `json:"correlation_id"` // TaskType 是发起观测的轮询任务类型,入库时用于审计上下文与流量评估标记。 TaskType string `json:"task_type"` Traffic *IngestTrafficPayload `json:"traffic,omitempty"` Network *IngestNetworkPayload `json:"network,omitempty"` Realname *IngestRealnamePayload `json:"realname,omitempty"` Raw *carddomain.GatewayRaw `json:"raw,omitempty"` PublishedAt time.Time `json:"published_at"` } // IngestTrafficPayload 是流量观测内容。 type IngestTrafficPayload struct { ReadingMB float64 `json:"reading_mb"` ReadingResult string `json:"reading_result"` ResetDay int `json:"reset_day"` DurationMS int `json:"duration_ms"` } // IngestNetworkPayload 是网络状态观测内容。 type IngestNetworkPayload struct { Status string `json:"status"` Extend string `json:"extend"` IMEI string `json:"imei"` } // IngestRealnamePayload 是实名观测内容。 type IngestRealnamePayload struct { Verified bool `json:"verified"` } // metadata 还原观测元数据。 func (m IngestMessage) metadata() carddomain.ObservationMetadata { return carddomain.ObservationMetadata{ ObservationID: m.ObservationID, Source: m.Source, Scene: m.Scene, ObservedAt: m.ObservedAt, CorrelationID: m.CorrelationID, } } // valid 校验消息必需字段。 func (m IngestMessage) valid() bool { if m.CardID == 0 || m.ObservationID == "" || m.ObservedAt.IsZero() { return false } switch m.Kind { case ingestKindTraffic: return m.Traffic != nil case ingestKindNetwork: return m.Network != nil case ingestKindRealname: return m.Realname != nil } return false } // CollectionMessage 是一条采集记录消息(topic card.collection),成功记录与失败记录二选一。 type CollectionMessage struct { Success *model.CardTrafficCollection `json:"success,omitempty"` Failure *model.CardTrafficCollectionFailure `json:"failure,omitempty"` } // MessagePublisher 发布 NSQ 消息。 type MessagePublisher interface { Publish(topic string, body []byte) error } // IngestPublisher 发布入库消息与采集记录消息。 type IngestPublisher struct { producer MessagePublisher } // NewIngestPublisher 创建入库消息发布器;producer 为 nil 时所有发布都返回错误,调用方降级同步入库。 func NewIngestPublisher(producer MessagePublisher) *IngestPublisher { return &IngestPublisher{producer: producer} } // PublishIngest 发布一条入库消息。 func (p *IngestPublisher) PublishIngest(_ context.Context, msg IngestMessage) error { if p == nil || p.producer == nil { return errors.New(errors.CodeInternalError, "入库消息发布器未配置") } if msg.Raw != nil && len(msg.Raw.Response) > ingestRawResponseLimit { trimmed := *msg.Raw trimmed.Response = trimmed.Response[:ingestRawResponseLimit] msg.Raw = &trimmed } msg.PublishedAt = time.Now() body, err := json.Marshal(msg) if err != nil { return errors.Wrap(errors.CodeInternalError, err, "编码入库消息失败") } if err := p.producer.Publish(constants.NSQCardIngestTopic, body); err != nil { return errors.Wrap(errors.CodeInternalError, err, "发布入库消息失败") } return nil } // PublishCollection 发布一条采集记录消息,实现卡观测服务的 CollectionSink。 func (p *IngestPublisher) PublishCollection(_ context.Context, success *model.CardTrafficCollection, failure *model.CardTrafficCollectionFailure) error { if p == nil || p.producer == nil { return errors.New(errors.CodeInternalError, "采集记录消息发布器未配置") } body, err := json.Marshal(CollectionMessage{Success: success, Failure: failure}) if err != nil { return errors.Wrap(errors.CodeInternalError, err, "编码采集记录消息失败") } if err := p.producer.Publish(constants.NSQCardCollectionTopic, body); err != nil { return errors.Wrap(errors.CodeInternalError, err, "发布采集记录消息失败") } return nil } // ingestRetryDelay 返回第 attempts 次投递失败后的重投延迟;超过序列长度取最后一项,不设次数上限。 func ingestRetryDelay(attempts uint16) time.Duration { delays := constants.PollingIngestRetryDelays index := int(attempts) - 1 if index < 0 { index = 0 } if index >= len(delays) { index = len(delays) - 1 } return delays[index] }