fix: 卡或绑定设备关闭轮询时不再自动停复机

关闭轮询的卡/设备已导入其他卡管,EvaluateAndAct 入口直接返回,不停机也不复机。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
邹进
2026-11-02 15:56:22 +09:00
parent e9d8c95b44
commit 76f7d47167
3 changed files with 95 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
package iot_card
import (
"context"
stderrors "errors"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// pollingDisabled 判断卡是否退出本系统自动停复机:卡级关闭轮询,或卡绑定的设备关闭轮询。
// 关闭轮询的卡/设备已交由其他卡管管理,本系统任何入口都不得再对其自动停机或复机。
// 查询失败时返回错误,由调用方放弃本次停复机,避免在状态不明时误操作。
func (s *StopResumeService) pollingDisabled(ctx context.Context, card *model.IotCard) (bool, error) {
if !card.EnablePolling {
return true, nil
}
if s.db == nil {
return false, nil
}
deviceID, bound, err := s.getCardDeviceID(ctx, card)
if err != nil {
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询卡绑定设备失败")
}
if !bound {
return false, nil
}
var device model.Device
if err := s.db.WithContext(ctx).Select("id", "enable_polling").First(&device, deviceID).Error; err != nil {
if stderrors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询设备轮询状态失败")
}
return !device.EnablePolling, nil
}

View File

@@ -0,0 +1,45 @@
package iot_card
import (
"context"
"testing"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// 关闭轮询的卡已交由其他卡管:无论在线还是停机,EvaluateAndAct 都必须直接返回,
// 不得进入停机原因判断或复机流程(服务未注入任何 store/网关,进入后续流程会直接 panic)。
func TestEvaluateAndAct_CardPollingDisabledSkipsStopResume(t *testing.T) {
svc := &StopResumeService{logger: zap.NewNop()}
cases := []struct {
name string
card *model.IotCard
}{
{
name: "在线卡无套餐不停机",
card: &model.IotCard{NetworkStatus: constants.NetworkStatusOnline, EnablePolling: false, IsStandalone: false},
},
{
name: "轮询原因停机的卡不复机",
card: &model.IotCard{NetworkStatus: constants.NetworkStatusOffline, StopReason: constants.StopReasonNoPackage, EnablePolling: false},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if err := svc.EvaluateAndAct(context.Background(), tc.card); err != nil {
t.Fatalf("期望直接返回 nil,实际 %v", err)
}
})
}
}
func TestEvaluateAndAct_NilCard(t *testing.T) {
svc := &StopResumeService{logger: zap.NewNop()}
if err := svc.EvaluateAndAct(context.Background(), nil); err != nil {
t.Fatalf("期望 nil,实际 %v", err)
}
}

View File

@@ -108,7 +108,19 @@ func (s *StopResumeService) SetPollingCallback(callback PollingCallback) {
// EvaluateAndAct 停复机统一入口
// 根据卡的当前网络状态,自动判断并执行停机或复机操作
// 设备/单卡维度通过 card.IsStandalone 推导(false 则为设备维度,停/复机覆盖设备下所有卡)
// 卡或其绑定设备关闭轮询时直接返回,不停机也不复机。
func (s *StopResumeService) EvaluateAndAct(ctx context.Context, card *model.IotCard) error {
if card == nil {
return nil
}
disabled, err := s.pollingDisabled(ctx, card)
if err != nil {
return err
}
if disabled {
s.logger.Debug("卡或绑定设备已关闭轮询,跳过自动停复机", zap.Uint("card_id", card.ID))
return nil
}
switch card.NetworkStatus {
case constants.NetworkStatusOnline:
// 卡在线,检查是否需要停机