// Package carrierthreshold 编排运营商通道流量阈值的达量判定与周期锁事实。 // // 本包拥有 tb_carrier_traffic_threshold_lock 的全部读写:达量判定在流量观测事务内写入周期锁 // 与可靠停机事件,消费者以提交认领字段取得至多一次的外部调用权,周期处理与恢复扫描只查询锁行。 // 本包不调用任何运营商接口,也不在数据库事务内发起外部 I/O:停复机执行通过 CardCommander // 端口复用既有停复机单一事实源。 package carrierthreshold import ( "context" "time" "go.uber.org/zap" "gorm.io/gorm" domain "github.com/break/junhong_cmp_fiber/internal/domain/carrierthreshold" "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/errors" ) // CardCommander 是通道阈值停复机对卡与运营商接口的能力边界。 // // 实现必须复用既有停复机单一事实源(重试、Integration Log、统一审计、卡状态观测序列)与 // 既有套餐/流量/实名/风险判定;本包绝不复制这些规则,也绝不直接调用运营商接口。 type CardCommander interface { // StopCardForThreshold 执行通道阈值达量停机,成功时写回卡停机状态与停因。 StopCardForThreshold(ctx context.Context, cardID uint) (domain.CommandOutcome, error) // ResumeCardForThreshold 执行新周期复机,成功时写回卡在线状态(只清除通道阈值停因)。 ResumeCardForThreshold(ctx context.Context, cardID uint) (domain.CommandOutcome, error) // ResumeReady 判断解锁后的卡是否满足自动复机条件;不满足时返回可安全记录的原因。 ResumeReady(ctx context.Context, cardID uint) (ready bool, reason string, err error) // CardNetworkStatus 只查询运营商卡状态并映射为本地网络状态;known 为 false 表示状态不可判定。 CardNetworkStatus(ctx context.Context, cardID uint) (status int, known bool, integrationID string, err error) // ConfirmCardState 按已确认的运营商结果补写卡状态,覆盖 Gateway 成功但 DB 更新失败的场景。 ConfirmCardState(ctx context.Context, cardID uint, offline bool) error } // Service 执行通道阈值达量判定并维护周期锁事实。 type Service struct { db *gorm.DB // repository 是公共 Outbox 仓储:达量停机事件与周期复机事件必须与锁事实在同一事务写入(ENG-OUTBOX-001)。 repository *outbox.Repository logger *zap.Logger // commander 是停复机执行端口;未注入时消费者与 cron 拒绝执行外部调用。 commander CardCommander now func() time.Time } // NewService 创建通道阈值用例;repository 决定达量判定能否写出可靠停复机事件。 func NewService(db *gorm.DB, repository *outbox.Repository) *Service { return &Service{db: db, repository: repository, logger: zap.NewNop(), now: time.Now} } // SetLogger 注入通道阈值运行日志。 func (s *Service) SetLogger(logger *zap.Logger) *Service { if s == nil { return s } if logger == nil { s.logger = zap.NewNop() return s } s.logger = logger return s } // SetCommander 注入停复机执行端口(消费者与周期处理必需)。 func (s *Service) SetCommander(commander CardCommander) *Service { if s == nil { return s } s.commander = commander return s } // ChannelThresholdLocked 判断该卡当前计费周期是否持有通道阈值停机锁。 // // 供复机入口前置拒绝复用:持锁即拒绝,判定口径与锁生效口径完全一致(按锁行自身运营商的 // data_reset_day 判断周期归属),不另立一套判定。 func (s *Service) ChannelThresholdLocked(ctx context.Context, cardID uint) (bool, error) { lock, err := s.ActiveLock(ctx, cardID, s.now()) if err != nil { return false, err } return lock != nil, nil } // carrierResetDays 按运营商 ID 集合显式查询上游流量重置日(ENG-MODEL-001:不使用关联标签)。 // 返回结果只包含仍然存在的运营商,缺失 ID 由调用方按各自语义处理。 func (s *Service) carrierResetDays(ctx context.Context, carrierIDs []uint) (map[uint]int, error) { resetDays := make(map[uint]int, len(carrierIDs)) if len(carrierIDs) == 0 { return resetDays, nil } var carriers []model.Carrier if err := s.db.WithContext(ctx). Select("id", "data_reset_day"). Where("id IN ?", carrierIDs). Find(&carriers).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询运营商上游流量重置日失败") } for index := range carriers { resetDays[carriers[index].ID] = carriers[index].DataResetDay } return resetDays, nil }