// Package cardobservation 提供卡观测的 Outbox、缓存和消费者适配器。 package cardobservation import ( "context" "strconv" "time" "github.com/bytedance/sonic" "github.com/redis/go-redis/v9" "go.uber.org/zap" "gorm.io/gorm" "gorm.io/gorm/clause" cardapp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation" "github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) func uintString(value uint) string { return strconv.FormatUint(uint64(value), 10) } // EventWriter 将卡实名状态变化写入公共 Outbox。 type EventWriter struct { outbox *outbox.Repository } // NewEventWriter 创建卡实名事件 Writer。 func NewEventWriter(repository *outbox.Repository) *EventWriter { return &EventWriter{outbox: repository} } // Append 在调用方事务中追加卡实名状态变化事件。 func (w *EventWriter) AppendRealname(ctx context.Context, tx *gorm.DB, event cardapp.RealnameChangedEvent) error { if w == nil || w.outbox == nil { return errors.New(errors.CodeInternalError, "卡实名 Outbox Writer 未配置") } _, err := w.outbox.Append(ctx, tx, outbox.Envelope{ EventID: event.EventID, EventType: constants.OutboxEventTypeCardRealnameChanged, PayloadVersion: constants.CardRealnameChangedPayloadVersionV1, AggregateType: "iot_card", AggregateID: uintString(event.CardID), ResourceType: constants.AssetTypeIotCard, ResourceID: uintString(event.CardID), BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID, Payload: event, }) return err } // AppendTraffic 在调用方事务中追加卡流量正增量事件。 func (w *EventWriter) AppendTraffic(ctx context.Context, tx *gorm.DB, event cardapp.TrafficIncrementedEvent) error { if w == nil || w.outbox == nil { return errors.New(errors.CodeInternalError, "卡流量 Outbox Writer 未配置") } _, err := w.outbox.Append(ctx, tx, outbox.Envelope{ EventID: event.EventID, EventType: constants.OutboxEventTypeCardTrafficIncremented, PayloadVersion: constants.CardTrafficIncrementedPayloadVersionV1, AggregateType: "iot_card", AggregateID: uintString(event.CardID), ResourceType: constants.AssetTypeIotCard, ResourceID: uintString(event.CardID), BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID, Payload: event, }) return err } // AppendNetwork 在调用方事务中追加卡网络状态变化事件。 func (w *EventWriter) AppendNetwork(ctx context.Context, tx *gorm.DB, event cardapp.NetworkChangedEvent) error { if w == nil || w.outbox == nil { return errors.New(errors.CodeInternalError, "卡网络 Outbox Writer 未配置") } _, err := w.outbox.Append(ctx, tx, outbox.Envelope{ EventID: event.EventID, EventType: constants.OutboxEventTypeCardNetworkChanged, PayloadVersion: constants.CardNetworkChangedPayloadVersionV1, AggregateType: "iot_card", AggregateID: uintString(event.CardID), ResourceType: constants.AssetTypeIotCard, ResourceID: uintString(event.CardID), BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID, Payload: event, }) return err } // CacheInvalidator 在提交后删除轮询卡缓存和旧 Redis 逆转计数。 type CacheInvalidator struct { redis *redis.Client logger *zap.Logger } // NewCacheInvalidator 创建卡观测缓存失效适配器。 func NewCacheInvalidator(redisClient *redis.Client, logger *zap.Logger) *CacheInvalidator { return &CacheInvalidator{redis: redisClient, logger: logger} } // Invalidate 删除可由数据库重建的缓存;失败只告警,不伪造事务回滚。 func (i *CacheInvalidator) Invalidate(ctx context.Context, cardID uint) { if i == nil || i.redis == nil { return } if err := i.redis.Del(ctx, constants.RedisPollingCardInfoKey(cardID), constants.RedisPollingRealnameReversalCountKey(cardID)).Err(); err != nil && i.logger != nil { i.logger.Warn("卡实名事实已提交但缓存失效失败", zap.Uint("card_id", cardID), zap.Error(err)) } } // RealnameActivator 激活卡或设备待实名套餐。 type RealnameActivator interface { ActivateByRealname(ctx context.Context, carrierType string, carrierID uint) error } // TrafficDeductor 按卡流量正增量扣减套餐流量。 type TrafficDeductor interface { DeductDataUsage(ctx context.Context, carrierType string, carrierID uint, usageMB float64) error } // StopResumeEvaluator 根据卡的当前事实执行停复机评估。 type StopResumeEvaluator interface { EvaluateAndAct(ctx context.Context, card *model.IotCard) error } // DeviceBindingReader 查询卡当前绑定的设备。 type DeviceBindingReader interface { GetActiveBindingByCardID(ctx context.Context, cardID uint) (*model.DeviceSimBinding, error) } // RealnameChangedConsumer 消费实名变化事件并串联现有幂等业务副作用。 type RealnameChangedConsumer struct { db *gorm.DB activator RealnameActivator binding DeviceBindingReader evaluator StopResumeEvaluator } // NetworkChangedConsumer 消费网络状态变化并按当前权威事实执行停复机评估。 type NetworkChangedConsumer struct { db *gorm.DB evaluator StopResumeEvaluator } // NewNetworkChangedConsumer 创建卡网络状态变化事件消费者。 func NewNetworkChangedConsumer(db *gorm.DB, evaluator StopResumeEvaluator) *NetworkChangedConsumer { return &NetworkChangedConsumer{db: db, evaluator: evaluator} } // Consume 校验网络事件并按当前卡事实执行评估。 func (c *NetworkChangedConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error { if c == nil || c.db == nil { return errors.New(errors.CodeInternalError, "卡网络事件消费者未配置") } ctx = cardapp.SuppressSeriesTriggerContext(ctx) if envelope.EventType != constants.OutboxEventTypeCardNetworkChanged || envelope.PayloadVersion != constants.CardNetworkChangedPayloadVersionV1 { return errors.New(errors.CodeInvalidParam, "卡网络事件类型或版本不受支持") } var event cardapp.NetworkChangedEvent if err := sonic.Unmarshal(envelope.Payload, &event); err != nil { return errors.Wrap(errors.CodeInvalidParam, err, "卡网络事件载荷无法解析") } if event.EventID != envelope.EventID || event.CardID == 0 || event.BeforeStatus == event.AfterStatus { return errors.New(errors.CodeInvalidParam, "卡网络事件载荷不完整") } if c.evaluator == nil { return nil } var card model.IotCard if err := c.db.WithContext(ctx).Where("id = ?", event.CardID).First(&card).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil } return errors.Wrap(errors.CodeDatabaseError, err, "查询网络变化后的卡事实失败") } return c.evaluator.EvaluateAndAct(ctx, &card) } // NewRealnameChangedConsumer 创建卡实名变化事件消费者。 func NewRealnameChangedConsumer(db *gorm.DB, activator RealnameActivator, binding DeviceBindingReader, evaluator StopResumeEvaluator) *RealnameChangedConsumer { return &RealnameChangedConsumer{db: db, activator: activator, binding: binding, evaluator: evaluator} } // Consume 校验载荷,并以当前权威卡事实执行可重入副作用。 func (c *RealnameChangedConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error { if c == nil || c.db == nil { return errors.New(errors.CodeInternalError, "卡实名事件消费者未配置") } ctx = cardapp.SuppressSeriesTriggerContext(ctx) if envelope.EventType != constants.OutboxEventTypeCardRealnameChanged || envelope.PayloadVersion != constants.CardRealnameChangedPayloadVersionV1 { return errors.New(errors.CodeInvalidParam, "卡实名事件类型或版本不受支持") } var event cardapp.RealnameChangedEvent if err := sonic.Unmarshal(envelope.Payload, &event); err != nil { return errors.Wrap(errors.CodeInvalidParam, err, "卡实名事件载荷无法解析") } if event.EventID != envelope.EventID || event.CardID == 0 || event.BeforeStatus == event.AfterStatus { return errors.New(errors.CodeInvalidParam, "卡实名事件载荷不完整") } if event.FirstVerified && c.activator != nil { if err := c.activator.ActivateByRealname(ctx, constants.AssetTypeIotCard, event.CardID); err != nil { return err } if c.binding != nil { binding, err := c.binding.GetActiveBindingByCardID(ctx, event.CardID) if err == nil && binding != nil { if err := c.activator.ActivateByRealname(ctx, constants.AssetTypeDevice, binding.DeviceID); err != nil { return err } } else if err != nil && err != gorm.ErrRecordNotFound { return errors.Wrap(errors.CodeDatabaseError, err, "查询实名卡绑定设备失败") } } } if c.evaluator == nil { return nil } var card model.IotCard if err := c.db.WithContext(ctx).Where("id = ?", event.CardID).First(&card).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil } return errors.Wrap(errors.CodeDatabaseError, err, "查询实名变化后的卡事实失败") } return c.evaluator.EvaluateAndAct(ctx, &card) } // TrafficIncrementedConsumer 消费卡流量正增量并推进幂等副作用进度。 type TrafficIncrementedConsumer struct { db *gorm.DB redis *redis.Client deductor TrafficDeductor evaluator StopResumeEvaluator } // NewTrafficIncrementedConsumer 创建卡流量正增量事件消费者。 func NewTrafficIncrementedConsumer(db *gorm.DB, redisClient *redis.Client, deductor TrafficDeductor, evaluator StopResumeEvaluator) *TrafficIncrementedConsumer { return &TrafficIncrementedConsumer{db: db, redis: redisClient, deductor: deductor, evaluator: evaluator} } // Consume 按事件处理进度避免至少一次投递重复扣减套餐流量。 func (c *TrafficIncrementedConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error { if c == nil || c.db == nil || c.redis == nil || c.deductor == nil { return errors.New(errors.CodeInternalError, "卡流量事件消费者未配置") } ctx = cardapp.SuppressSeriesTriggerContext(ctx) if envelope.EventType != constants.OutboxEventTypeCardTrafficIncremented || envelope.PayloadVersion != constants.CardTrafficIncrementedPayloadVersionV1 { return errors.New(errors.CodeInvalidParam, "卡流量事件类型或版本不受支持") } var event cardapp.TrafficIncrementedEvent if err := sonic.Unmarshal(envelope.Payload, &event); err != nil { return errors.Wrap(errors.CodeInvalidParam, err, "卡流量事件载荷无法解析") } if event.EventID != envelope.EventID || event.CardID == 0 || event.IncrementMB <= 0 { return errors.New(errors.CodeInvalidParam, "卡流量事件载荷不完整") } receipt, err := c.acquireTrafficEffect(ctx, event.EventID) if err != nil { return err } if receipt.Status == constants.CardObservationEffectStatusCompleted { return nil } if receipt.Status == constants.CardObservationEffectStatusProcessing { dailyKey := constants.RedisCardDailyTrafficKey(event.CardID, event.ObservedAt.Format("2006-01-02")) pipe := c.redis.TxPipeline() pipe.IncrByFloat(ctx, dailyKey, event.IncrementMB) pipe.Expire(ctx, dailyKey, 48*time.Hour) if _, err := pipe.Exec(ctx); err != nil { return errors.Wrap(errors.CodeInternalError, err, "记录卡日流量缓冲失败,处理结果未知") } if err := c.updateTrafficEffectStatus(ctx, receipt.ID, constants.CardObservationEffectStatusProcessing, constants.CardObservationEffectStatusDailySaved); err != nil { return err } receipt.Status = constants.CardObservationEffectStatusDailySaved } if receipt.Status == constants.CardObservationEffectStatusDailySaved { if err := c.updateTrafficEffectStatus(ctx, receipt.ID, constants.CardObservationEffectStatusDailySaved, constants.CardObservationEffectStatusProcessing); err != nil { return err } if err := c.deductor.DeductDataUsage(ctx, constants.AssetTypeIotCard, event.CardID, event.IncrementMB); err != nil { return err } if err := c.updateTrafficEffectStatus(ctx, receipt.ID, constants.CardObservationEffectStatusProcessing, constants.CardObservationEffectStatusDeducted); err != nil { return err } receipt.Status = constants.CardObservationEffectStatusDeducted } if c.evaluator != nil { var card model.IotCard if err := c.db.WithContext(ctx).Where("id = ?", event.CardID).First(&card).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询流量变化后的卡事实失败") } if err := c.evaluator.EvaluateAndAct(ctx, &card); err != nil { return err } } if err := c.updateTrafficEffectStatus(ctx, receipt.ID, constants.CardObservationEffectStatusDeducted, constants.CardObservationEffectStatusCompleted); err != nil { return err } return nil } func (c *TrafficIncrementedConsumer) acquireTrafficEffect(ctx context.Context, eventID string) (*model.CardObservationEffect, error) { var receipt model.CardObservationEffect err := c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("event_id = ?", eventID).First(&receipt).Error if err == gorm.ErrRecordNotFound { receipt = model.CardObservationEffect{ EventID: eventID, EffectType: constants.CardObservationEffectTypeTrafficIncrement, Status: constants.CardObservationEffectStatusProcessing, } return tx.Create(&receipt).Error } if err != nil { return err } switch receipt.Status { case constants.CardObservationEffectStatusCompleted, constants.CardObservationEffectStatusDailySaved, constants.CardObservationEffectStatusDeducted: return nil case constants.CardObservationEffectStatusProcessing: return errors.New(errors.CodeConflict, "卡流量副作用仍在处理或结果未知") default: return tx.Model(&receipt).Where("status = ?", constants.CardObservationEffectStatusPending). Update("status", constants.CardObservationEffectStatusProcessing).Error } }) if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "获取卡流量副作用处理权失败") } return &receipt, nil } func (c *TrafficIncrementedConsumer) updateTrafficEffectStatus(ctx context.Context, id uint, beforeStatus, afterStatus int) error { result := c.db.WithContext(ctx).Model(&model.CardObservationEffect{}). Where("id = ? AND status = ?", id, beforeStatus). Update("status", afterStatus) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新卡流量副作用进度失败") } if result.RowsAffected != 1 { return errors.New(errors.CodeConflict, "卡流量副作用进度已被其他消费者更新") } return nil }