package iot_card import ( "context" "time" "github.com/redis/go-redis/v9" "go.uber.org/zap" "gorm.io/gorm" stderrors "errors" "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" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // StopResumeService 停复机服务 // 处理 IoT 卡的自动停机、复机和手动停复机逻辑 type StopResumeService struct { db *gorm.DB redis *redis.Client iotCardStore *postgres.IotCardStore deviceSimBindingStore *postgres.DeviceSimBindingStore gatewayClient *gateway.Client logger *zap.Logger maxRetries int retryInterval time.Duration } // NewStopResumeService 创建停复机服务 func NewStopResumeService( db *gorm.DB, redis *redis.Client, iotCardStore *postgres.IotCardStore, deviceSimBindingStore *postgres.DeviceSimBindingStore, gatewayClient *gateway.Client, logger *zap.Logger, ) *StopResumeService { return &StopResumeService{ db: db, redis: redis, iotCardStore: iotCardStore, deviceSimBindingStore: deviceSimBindingStore, gatewayClient: gatewayClient, logger: logger, maxRetries: 3, retryInterval: 2 * time.Second, } } // 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 } // ManualStopCard 手动停机单张卡(通过ICCID) func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) error { card, err := s.iotCardStore.GetByICCID(ctx, iccid) if err != nil { return errors.New(errors.CodeNotFound, "卡不存在") } if card.RealNameStatus != constants.RealNameStatusVerified { return errors.New(errors.CodeForbidden, "卡未实名,无法操作") } // 检查绑定设备是否在复机保护期 if s.deviceSimBindingStore != nil && s.redis != nil { binding, bindErr := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID) if bindErr == nil && binding != nil { exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(binding.DeviceID, "start")).Result() if exists > 0 { return errors.New(errors.CodeForbidden, "设备复机保护期内,禁止停机") } } else if bindErr != nil && !stderrors.Is(bindErr, gorm.ErrRecordNotFound) { return errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败") } } if err := s.stopCardWithRetry(ctx, card); err != nil { return errors.Wrap(errors.CodeInternalError, err, "调网关停机失败") } now := time.Now() return s.db.WithContext(ctx).Model(card).Updates(map[string]any{ "network_status": constants.NetworkStatusOffline, "stopped_at": now, "stop_reason": constants.StopReasonManual, }).Error } // ManualStartCard 手动复机单张卡(通过ICCID) func (s *StopResumeService) ManualStartCard(ctx context.Context, iccid string) error { card, err := s.iotCardStore.GetByICCID(ctx, iccid) if err != nil { return errors.New(errors.CodeNotFound, "卡不存在") } if card.RealNameStatus != constants.RealNameStatusVerified { return errors.New(errors.CodeForbidden, "卡未实名,无法操作") } // 检查绑定设备是否在停机保护期 if s.deviceSimBindingStore != nil && s.redis != nil { binding, bindErr := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID) if bindErr == nil && binding != nil { exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(binding.DeviceID, "stop")).Result() if exists > 0 { return errors.New(errors.CodeForbidden, "设备停机保护期内,禁止复机") } } else if bindErr != nil && !stderrors.Is(bindErr, gorm.ErrRecordNotFound) { return errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败") } } if err := s.resumeCardWithRetry(ctx, card); err != nil { return errors.Wrap(errors.CodeInternalError, err, "调网关复机失败") } now := time.Now() return s.db.WithContext(ctx).Model(card).Updates(map[string]any{ "network_status": constants.NetworkStatusOnline, "resumed_at": now, "stop_reason": "", }).Error }