package iot_card import ( "context" "time" "github.com/redis/go-redis/v9" "go.uber.org/zap" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/gateway" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" ) // StopResumeService 停复机服务 // 任务 24.2: 处理 IoT 卡的自动停机和复机逻辑 type StopResumeService struct { db *gorm.DB redis *redis.Client iotCardStore *postgres.IotCardStore gatewayClient *gateway.Client logger *zap.Logger // 重试配置 maxRetries int retryInterval time.Duration } // NewStopResumeService 创建停复机服务 func NewStopResumeService( db *gorm.DB, redis *redis.Client, iotCardStore *postgres.IotCardStore, gatewayClient *gateway.Client, logger *zap.Logger, ) *StopResumeService { return &StopResumeService{ db: db, redis: redis, iotCardStore: iotCardStore, gatewayClient: gatewayClient, logger: logger, maxRetries: 3, // 默认最多重试 3 次 retryInterval: 2 * time.Second, // 默认重试间隔 2 秒 } } // CheckAndStopCard 任务 24.3: 检查流量耗尽并停机 // 当所有套餐流量用完时,调用运营商接口停机 func (s *StopResumeService) CheckAndStopCard(ctx context.Context, cardID uint) error { // 查询卡信息 card, err := s.iotCardStore.GetByID(ctx, cardID) if err != nil { return err } // 如果已经是停机状态,跳过 if card.NetworkStatus == constants.NetworkStatusOffline { s.logger.Debug("卡已处于停机状态,跳过", zap.Uint("card_id", cardID)) return nil } // 检查是否有可用套餐(status=1 生效中 或 status=0 待生效) hasAvailablePackage, err := s.hasAvailablePackage(ctx, cardID) if err != nil { return err } // 如果还有可用套餐,不停机 if hasAvailablePackage { return nil } // 任务 24.5: 调用运营商停机接口(带重试机制) if err := s.stopCardWithRetry(ctx, card); err != nil { s.logger.Error("调用运营商停机接口失败", zap.Uint("card_id", cardID), zap.String("iccid", card.ICCID), zap.Error(err)) return err } // 更新卡状态 now := time.Now() if err := s.db.WithContext(ctx).Model(card).Updates(map[string]any{ "network_status": constants.NetworkStatusOffline, "stopped_at": now, "stop_reason": constants.StopReasonTrafficExhausted, }).Error; err != nil { return err } s.logger.Info("卡因流量耗尽已停机", zap.Uint("card_id", cardID), zap.String("iccid", card.ICCID)) return nil } // ResumeCardIfStopped 任务 24.4: 购买套餐后自动复机 // 当购买新套餐且卡之前因流量耗尽停机时,自动复机 func (s *StopResumeService) ResumeCardIfStopped(ctx context.Context, cardID uint) error { // 查询卡信息 card, err := s.iotCardStore.GetByID(ctx, cardID) if err != nil { return err } // 幂等性检查:如果已经是开机状态,跳过 if card.NetworkStatus == constants.NetworkStatusOnline { s.logger.Debug("卡已处于开机状态,跳过", zap.Uint("card_id", cardID)) return nil } // 只有因流量耗尽停机的卡才自动复机 if card.StopReason != constants.StopReasonTrafficExhausted { s.logger.Debug("卡非流量耗尽停机,不自动复机", zap.Uint("card_id", cardID), zap.String("stop_reason", card.StopReason)) return nil } // 任务 24.5: 调用运营商复机接口(带重试机制) if err := s.resumeCardWithRetry(ctx, card); err != nil { s.logger.Error("调用运营商复机接口失败", zap.Uint("card_id", cardID), zap.String("iccid", card.ICCID), zap.Error(err)) return err } // 更新卡状态 now := time.Now() if err := s.db.WithContext(ctx).Model(card).Updates(map[string]any{ "network_status": constants.NetworkStatusOnline, "resumed_at": now, "stop_reason": "", // 清空停机原因 }).Error; err != nil { return err } s.logger.Info("卡购买套餐后已自动复机", zap.Uint("card_id", cardID), zap.String("iccid", card.ICCID)) return nil } // hasAvailablePackage 检查是否有可用套餐 func (s *StopResumeService) hasAvailablePackage(ctx context.Context, cardID uint) (bool, error) { var count int64 err := s.db.WithContext(ctx).Model(&model.PackageUsage{}). Where("iot_card_id = ?", cardID). Where("status IN ?", []int{ constants.PackageUsageStatusPending, // 待生效 constants.PackageUsageStatusActive, // 生效中 }). Count(&count).Error if err != nil { return false, err } return count > 0, nil } // stopCardWithRetry 任务 24.5: 调用运营商停机接口(带重试机制) func (s *StopResumeService) stopCardWithRetry(ctx context.Context, card *model.IotCard) error { if s.gatewayClient == nil { s.logger.Warn("Gateway 客户端未配置,跳过调用运营商接口", zap.Uint("card_id", card.ID)) return nil } var lastErr error for i := 0; i < s.maxRetries; i++ { if i > 0 { s.logger.Debug("重试调用停机接口", zap.Int("attempt", i+1), zap.String("iccid", card.ICCID)) time.Sleep(s.retryInterval) } err := s.gatewayClient.StopCard(ctx, &gateway.CardOperationReq{ CardNo: card.ICCID, }) if err == nil { return nil } lastErr = err s.logger.Warn("调用停机接口失败,准备重试", zap.Int("attempt", i+1), zap.Error(err)) } return lastErr } // resumeCardWithRetry 任务 24.5: 调用运营商复机接口(带重试机制) func (s *StopResumeService) resumeCardWithRetry(ctx context.Context, card *model.IotCard) error { if s.gatewayClient == nil { s.logger.Warn("Gateway 客户端未配置,跳过调用运营商接口", zap.Uint("card_id", card.ID)) return nil } var lastErr error for i := 0; i < s.maxRetries; i++ { if i > 0 { s.logger.Debug("重试调用复机接口", zap.Int("attempt", i+1), zap.String("iccid", card.ICCID)) time.Sleep(s.retryInterval) } err := s.gatewayClient.StartCard(ctx, &gateway.CardOperationReq{ CardNo: card.ICCID, }) if err == nil { return nil } lastErr = err s.logger.Warn("调用复机接口失败,准备重试", zap.Int("attempt", i+1), zap.Error(err)) } return lastErr }