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

99 lines
3.8 KiB
Go

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
}