feat: 停复机指令队列与设备维度判断
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled

自动停复机统一经停复机指令队列执行(全局开关 polling.stop_resume.queue_enabled,默认关闭):
- 统一判断函数 decideCard,新旧路径共用;暂停轮询的卡在套餐到期、退款场景照常停机
- 同卡一条指令、版本号覆盖;领取校验单卡最小重试间隔;先于卡任务领取,不受 scheduler_mode 限制
- 执行器只调用一次网关,按错误分类退避与告警,结果未知在队列内确认
- 设备维度判断合并请求与场景,版本变化再判断,旧路径持设备锁时推后
- 套餐激活、重置、订单、流量扣减、套餐到期、退款、实名管控修改改为请求评估
- 监控增加停复机队列与近 1 小时执行计数,单卡轮询状态返回当前指令
- 迁移 000255 插入开关与告警、退避参数

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
邹进
2026-10-26 00:36:26 +09:00
parent 1369fed642
commit 934a3a27a1
31 changed files with 2275 additions and 117 deletions

View File

@@ -0,0 +1,70 @@
package task
import (
"context"
"encoding/json"
"time"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/polling/carrierqueue"
)
// 设备判断的推后时长:旧路径持设备锁时、判断失败时。
const (
deviceEvalLegacyLockDelay = 5 * time.Second
deviceEvalFailureDelay = 30 * time.Second
deviceEvalTimeout = 30 * time.Second
)
// DeviceEvalExecutor 设备维度判断执行器:抢设备判断锁,读取最新数据为设备下各卡生成指令;锁内不调网关。
type DeviceEvalExecutor struct {
store *carrierqueue.Store
service StopResumeCommandService
instanceID string
logger *zap.Logger
}
// NewDeviceEvalExecutor 创建设备判断执行器。
func NewDeviceEvalExecutor(store *carrierqueue.Store, service StopResumeCommandService, instanceID string, logger *zap.Logger) *DeviceEvalExecutor {
if logger == nil {
logger = zap.NewNop()
}
return &DeviceEvalExecutor{store: store, service: service, instanceID: instanceID, logger: logger}
}
// Handle 消费一条设备判断消息。收尾只移除本次已处理的场景;版本变大时用最新数据再判断一次。
func (e *DeviceEvalExecutor) Handle(ctx context.Context, body []byte, _ func()) {
var msg carrierqueue.DeviceEvalMessage
if err := json.Unmarshal(body, &msg); err != nil || msg.DeviceID == 0 || msg.Token == "" {
e.logger.Warn("设备判断消息无效,丢弃", zap.ByteString("body", body))
return
}
got, err := e.store.AcquireDeviceEvalLock(ctx, msg.DeviceID, msg.Token, msg.Token+"|"+e.instanceID, 2*deviceEvalTimeout)
if err != nil || got != 1 {
return
}
claimed := carrierqueue.DeviceEvalClaimed{DeviceID: msg.DeviceID, Token: msg.Token, Version: msg.Version, Scenes: msg.Scenes}
deferFor := time.Duration(0)
if e.service.LegacyDeviceOpLocked(ctx, msg.DeviceID) {
// 旧路径正在对该设备执行停复机:推后,不并行判断。
deferFor = deviceEvalLegacyLockDelay
} else {
evalCtx, cancel := context.WithTimeout(ctx, deviceEvalTimeout)
if evalErr := e.service.EvaluateDeviceCommands(evalCtx, msg.DeviceID, msg.Scenes); evalErr != nil {
e.logger.Warn("设备判断失败,稍后再判断", zap.Uint("device_id", msg.DeviceID), zap.Strings("scenes", msg.Scenes), zap.Error(evalErr))
deferFor = deviceEvalFailureDelay
}
cancel()
}
finishCtx, finishCancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
defer finishCancel()
result, err := e.store.FinishDeviceEval(finishCtx, claimed, deferFor)
if err != nil {
e.logger.Warn("设备判断收尾失败,等待租约到期后重新领取", zap.Uint("device_id", msg.DeviceID), zap.Error(err))
return
}
if result == 2 {
e.logger.Info("设备判断期间有新请求,按最新数据再判断", zap.Uint("device_id", msg.DeviceID))
}
}