fix: 停复机指令队列审查问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m24s

- 开关开启但分发器心跳缺失时回退为当场执行,避免指令无人消费
- 待确认指令先查上游状态再判断;执行期间被覆盖时把待确认带到新指令
- 同方向失败退避中重复写入不提前执行时间
- 领取容量把执行上限与本秒剩余 QPS 分开计算
- 队列复机共用单卡复机锁;执行上下文取消后按结果未知确认
- 消费者并行停止;读取最近调用时间失败时不覆盖;成功但终结日志失败按成功处理

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
邹进
2026-10-26 00:43:58 +09:00
parent 934a3a27a1
commit a4e3fe4d8f
9 changed files with 188 additions and 79 deletions

View File

@@ -238,7 +238,11 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateCarrierReq
func (s *Service) requestRealnameControlEvaluation(ctx context.Context, carrierID uint, enabled bool) {
evalCtx := context.WithoutCancel(ctx)
go func() {
defer func() { _ = recover() }()
defer func() {
if r := recover(); r != nil {
s.logger.Error("实名管控修改后批量请求停复机评估 panic", zap.Uint("carrier_id", carrierID), zap.Any("panic", r))
}
}()
if _, err := s.realnameEvaluator.RequestRealnameControlEvaluation(evalCtx, carrierID, enabled); err != nil {
s.logger.Error("实名管控修改后批量请求停复机评估失败", zap.Uint("carrier_id", carrierID), zap.Error(err))
}

View File

@@ -52,6 +52,9 @@ func (s *StopResumeService) DecideCommand(ctx context.Context, cardID uint, scen
return CommandDecision{Card: card, Action: decision.Action, Reason: decision.Reason}, nil
}
// ErrCommandBusy 表示该卡复机正在其他路径执行(持单卡复机锁),本次未调用上游。
var ErrCommandBusy = stderrors.New("该卡复机正在执行中")
// CommandCallResult 是一次停复机网关调用的结果。
type CommandCallResult struct {
// Unknown 为 true 表示结果未知:超时、连接失败,或上游已成功但本地回写失败。
@@ -66,8 +69,22 @@ func (s *StopResumeService) ExecuteCommand(ctx context.Context, card *model.IotC
}
if action == constants.StopResumeActionStop {
outcome, err := s.stopCardAttempts(ctx, card, reason, 1)
if err != nil && outcome.Result == constants.AuditResultSuccess {
// 停机与回写已成功,只是 Integration Log 终结失败:按成功处理。
s.logger.Warn("停机成功但终结 Integration Log 失败", zap.Uint("card_id", card.ID), zap.Error(err))
return CommandCallResult{}, nil
}
return CommandCallResult{Unknown: err != nil && outcome.Result == constants.AuditResultUnknown && !gateway.IsRateLimited(err)}, err
}
// 与旧复机路径共用单卡复机锁,避免与自动续费、通道阈值等直接复机并发。
if s.redis != nil {
lockKey := constants.RedisCardResumeLockKey(card.ID)
locked, lockErr := s.redis.SetNX(ctx, lockKey, "1", 30*time.Second).Result()
if lockErr != nil || !locked {
return CommandCallResult{}, ErrCommandBusy
}
defer s.redis.Del(context.WithoutCancel(ctx), lockKey)
}
actionCode, summary := constants.AuditActionIotCardAutoStarted, "自动恢复 IoT 卡网络"
attempt, integrationID, err := s.resumeCardAttempts(ctx, card, actionCode, summary, 1)
if err != nil {

View File

@@ -2,6 +2,7 @@ package iot_card
import (
"context"
stderrors "errors"
"go.uber.org/zap"
@@ -62,11 +63,15 @@ func (s *StopResumeService) legacyAssetEvaluation(ctx context.Context, assetType
if err != nil {
return err
}
var failed []error
for _, card := range cards {
if err := s.EvaluateAndAct(ctx, card); err != nil {
return err
failed = append(failed, err)
}
}
if len(failed) > 0 {
return errors.Wrap(errors.CodeInternalError, stderrors.Join(failed...), "设备停复机评估存在未成功处理的卡")
}
return nil
}
card, err := s.iotCardStore.GetByID(ctx, assetID)