package cardobservation import ( "context" "strconv" "time" domain "github.com/break/junhong_cmp_fiber/internal/domain/cardobservation" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/errors" "gorm.io/gorm" "gorm.io/gorm/clause" ) // NetworkChangedEvent 是卡网络状态变化的可靠领域事实。 type NetworkChangedEvent struct { EventID string `json:"event_id"` CardID uint `json:"card_id"` BeforeStatus int `json:"before_status"` AfterStatus int `json:"after_status"` GatewayExtend string `json:"gateway_extend"` ObservedAt time.Time `json:"observed_at"` Source string `json:"source"` Scene string `json:"scene"` RequestID string `json:"request_id,omitempty"` CorrelationID string `json:"correlation_id,omitempty"` } // ApplyNetworkObservation 串行应用 Gateway 网络状态、扩展原因、IMEI 和风险轮询规则。 func (s *Service) ApplyNetworkObservation(ctx context.Context, observation domain.NetworkObservation) (domain.NetworkDecision, error) { if s == nil || s.db == nil || s.eventWriter == nil { return domain.NetworkDecision{}, errors.New(errors.CodeInternalError, "卡网络观测能力未完整配置") } var decision domain.NetworkDecision err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var card model.IotCard if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", observation.CardID).First(&card).Error; err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeNotFound, "IoT卡不存在") } return errors.Wrap(errors.CodeDatabaseError, err, "锁定IoT卡网络事实失败") } nextDecision, decisionErr := domain.ApplyNetwork(domain.CardNetworkSnapshot{ CardID: card.ID, NetworkStatus: card.NetworkStatus, StopReason: card.StopReason, IsStandalone: card.IsStandalone, EnablePolling: card.EnablePolling, }, observation) if decisionErr != nil { return decisionErr } decision = nextDecision updates := map[string]any{ "last_card_status_check_at": observation.Metadata.ObservedAt, "last_sync_time": observation.Metadata.ObservedAt, "gateway_extend": decision.GatewayExtend, } if decision.UpdateIMEI { updates["gateway_card_imei"] = decision.GatewayIMEI } if decision.StatusChanged { updates["network_status"] = decision.AfterStatus } if decision.StopReasonChanged { updates["stop_reason"] = decision.StopReason } if decision.StopPolling { updates["enable_polling"] = false } result := tx.Model(&model.IotCard{}). Where("id = ? AND network_status = ? AND enable_polling = ?", card.ID, card.NetworkStatus, card.EnablePolling). Updates(updates) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新卡网络事实失败") } if result.RowsAffected != 1 { return errors.New(errors.CodeConflict, "卡网络事实已被其他请求更新") } if !decision.StatusChanged { return nil } eventID := "card-network:" + strconv.FormatUint(uint64(card.ID), 10) + ":" + observation.Metadata.ObservationID + ":changed" if err := s.eventWriter.AppendNetwork(ctx, tx, NetworkChangedEvent{ EventID: eventID, CardID: card.ID, BeforeStatus: card.NetworkStatus, AfterStatus: decision.AfterStatus, GatewayExtend: decision.GatewayExtend, ObservedAt: observation.Metadata.ObservedAt, Source: observation.Metadata.Source, Scene: observation.Metadata.Scene, RequestID: observation.Metadata.RequestID, CorrelationID: observation.Metadata.CorrelationID, }); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "写入卡网络 Outbox 事件失败") } return nil }) if err != nil { return domain.NetworkDecision{}, err } if s.cache != nil { s.cache.Invalidate(ctx, observation.CardID) } return decision, nil }