fix: ResumeCardIfStopped 扩展 — 支持 device 类型、实名检查、并发锁
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -105,23 +105,40 @@ func (s *StopResumeService) CheckAndStopCard(ctx context.Context, cardID uint) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResumeCardIfStopped 任务 24.4: 购买套餐后自动复机
|
||||
// 当购买新套餐且卡之前因流量耗尽停机时,自动复机
|
||||
func (s *StopResumeService) ResumeCardIfStopped(ctx context.Context, cardID uint) error {
|
||||
// 查询卡信息
|
||||
// ResumeCardIfStopped 套餐激活或流量重置后自动复机入口
|
||||
// 支持 iot_card 和 device 两种载体类型:
|
||||
// - iot_card:对单张卡执行实名检查 → 停机原因检查 → 分布式锁 → Gateway → 更新 DB
|
||||
// - device:查询设备下所有已绑定卡,逐一执行单卡复机逻辑
|
||||
func (s *StopResumeService) ResumeCardIfStopped(ctx context.Context, carrierType string, carrierID uint) error {
|
||||
switch carrierType {
|
||||
case "iot_card":
|
||||
return s.resumeSingleCard(ctx, carrierID)
|
||||
case "device":
|
||||
return s.resumeDeviceCards(ctx, carrierID)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// resumeSingleCard 对单张卡执行复机逻辑
|
||||
// 依次检查:已开机则跳过 → 未实名则跳过(WARN)→ 非流量耗尽停机则跳过 → 加锁 → 调 Gateway → 更新 DB
|
||||
func (s *StopResumeService) resumeSingleCard(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.RealNameStatus != constants.RealNameStatusVerified {
|
||||
s.logger.Warn("未实名卡跳过自动复机",
|
||||
zap.Uint("card_id", cardID),
|
||||
zap.String("iccid", card.ICCID))
|
||||
return nil
|
||||
}
|
||||
|
||||
if card.StopReason != constants.StopReasonTrafficExhausted {
|
||||
s.logger.Debug("卡非流量耗尽停机,不自动复机",
|
||||
zap.Uint("card_id", cardID),
|
||||
@@ -129,7 +146,21 @@ func (s *StopResumeService) ResumeCardIfStopped(ctx context.Context, cardID uint
|
||||
return nil
|
||||
}
|
||||
|
||||
// 任务 24.5: 调用运营商复机接口(带重试机制)
|
||||
// 分布式锁防止同一张卡并发触发多次 Gateway 调用,TTL 30s
|
||||
lockKey := constants.RedisCardResumeLockKey(cardID)
|
||||
locked, lockErr := s.redis.SetNX(ctx, lockKey, "1", 30*time.Second).Result()
|
||||
if lockErr != nil {
|
||||
s.logger.Warn("获取复机分布式锁失败,跳过本次复机",
|
||||
zap.Uint("card_id", cardID),
|
||||
zap.Error(lockErr))
|
||||
return nil
|
||||
}
|
||||
if !locked {
|
||||
s.logger.Debug("复机操作进行中,跳过", zap.Uint("card_id", cardID))
|
||||
return nil
|
||||
}
|
||||
defer s.redis.Del(ctx, lockKey)
|
||||
|
||||
if err := s.resumeCardWithRetry(ctx, card); err != nil {
|
||||
s.logger.Error("调用运营商复机接口失败",
|
||||
zap.Uint("card_id", cardID),
|
||||
@@ -138,23 +169,50 @@ func (s *StopResumeService) ResumeCardIfStopped(ctx context.Context, cardID uint
|
||||
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": "", // 清空停机原因
|
||||
"stop_reason": "",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
// Gateway 成功但 DB 更新失败:记录 ERROR 供人工排查,不阻断流程
|
||||
s.logger.Error("复机 Gateway 成功但 DB 更新失败",
|
||||
zap.Uint("card_id", cardID),
|
||||
zap.String("iccid", card.ICCID),
|
||||
zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Info("卡购买套餐后已自动复机",
|
||||
s.logger.Info("卡已自动复机",
|
||||
zap.Uint("card_id", cardID),
|
||||
zap.String("iccid", card.ICCID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resumeDeviceCards 对设备绑定的所有已绑定卡逐一执行复机逻辑
|
||||
// 部分卡实名、部分未实名时:已实名卡复机,未实名卡跳过,互不影响
|
||||
func (s *StopResumeService) resumeDeviceCards(ctx context.Context, deviceID uint) error {
|
||||
bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, deviceID)
|
||||
if err != nil {
|
||||
s.logger.Warn("查询设备绑定卡失败",
|
||||
zap.Uint("device_id", deviceID),
|
||||
zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, binding := range bindings {
|
||||
if resumeErr := s.resumeSingleCard(ctx, binding.IotCardID); resumeErr != nil {
|
||||
s.logger.Warn("设备绑定卡复机失败,继续处理其他卡",
|
||||
zap.Uint("device_id", deviceID),
|
||||
zap.Uint("card_id", binding.IotCardID),
|
||||
zap.Error(resumeErr))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasAvailablePackage 检查是否有可用套餐
|
||||
func (s *StopResumeService) hasAvailablePackage(ctx context.Context, cardID uint) (bool, error) {
|
||||
var count int64
|
||||
|
||||
Reference in New Issue
Block a user