diff --git a/internal/service/iot_card/stop_resume_decide.go b/internal/service/iot_card/stop_resume_decide.go index e4a17747..8b8020fa 100644 --- a/internal/service/iot_card/stop_resume_decide.go +++ b/internal/service/iot_card/stop_resume_decide.go @@ -28,8 +28,8 @@ type stopResumeDecision struct { DeviceID uint // 设备维度动作的设备;0 表示单卡 } -// decideCard 统一判断函数:暂停轮询(按场景) → 停机条件 → 复机条件。 -// 暂停轮询的卡只在套餐失效类场景(套餐到期、退款)下放行无套餐停机,其余自动停复机一律不做; +// decideCard 统一判断函数:暂停轮询 → 停机条件 → 复机条件。 +// 卡或绑定设备暂停轮询(已交由其他卡管)时任何场景都不停机也不复机; // 套餐失效类场景不看库里的开停状态,资产无可用主套餐即停机。 // 设备维度原因(无套餐、流量耗尽)与绑定设备的复机返回 DeviceID,由调用方按设备处理。通道阈值锁由复机执行方判断。 func (s *StopResumeService) decideCard(ctx context.Context, card *model.IotCard, scenes []string) (stopResumeDecision, error) { @@ -37,11 +37,10 @@ func (s *StopResumeService) decideCard(ctx context.Context, card *model.IotCard, if err != nil { return stopResumeDecision{}, err } - allowSuspendedStop := constants.StopResumeSceneAllowsSuspendedStop(scenes) - if suspended && !allowSuspendedStop { + if suspended { return stopResumeDecision{}, nil } - if allowSuspendedStop { + if constants.StopResumeSceneIgnoresNetworkStatus(scenes) { // 套餐失效类场景(套餐到期、退款):不看库里的开停状态,资产无可用主套餐即停机。 // 停复机成功后上游状态有延迟,库里的开停状态可能是旧值;上游已停时调用按已是目标状态结束。 reasons, err := s.checkStopReasons(ctx, card) @@ -55,9 +54,6 @@ func (s *StopResumeService) decideCard(ctx context.Context, card *model.IotCard, } return decision, nil } - if suspended { - return stopResumeDecision{}, nil - } } switch card.NetworkStatus { case constants.NetworkStatusOnline: @@ -66,22 +62,12 @@ func (s *StopResumeService) decideCard(ctx context.Context, card *model.IotCard, return stopResumeDecision{}, err } primary := reasons[0] - if suspended { - // 暂停轮询:只有套餐失效(无可用主套餐)才停机。 - if !containsStopReason(reasons, constants.StopReasonNoPackage) { - return stopResumeDecision{}, nil - } - primary = constants.StopReasonNoPackage - } decision := stopResumeDecision{Action: constants.StopResumeActionStop, Reason: primary, Reasons: reasons} if !card.IsStandalone && isDeviceScopeReason(primary) { decision.DeviceID = s.boundDeviceID(ctx, card) } return decision, nil case constants.NetworkStatusOffline: - if suspended { - return stopResumeDecision{}, nil - } // 不看停机原因,是否复机只由本系统规则决定。 resume, err := s.shouldResume(ctx, card) if err != nil || !resume { diff --git a/internal/service/iot_card/stop_resume_polling_guard_test.go b/internal/service/iot_card/stop_resume_polling_guard_test.go new file mode 100644 index 00000000..03ddada0 --- /dev/null +++ b/internal/service/iot_card/stop_resume_polling_guard_test.go @@ -0,0 +1,48 @@ +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" +) + +// 关闭轮询的卡已交由其他卡管:任何场景(含套餐到期、退款)都不停机也不复机, +// 不得进入停机原因判断或复机流程(服务未注入任何 store/网关,进入后续流程会直接 panic)。 +func TestDecideCard_PollingDisabledNeverActs(t *testing.T) { + svc := &StopResumeService{logger: zap.NewNop()} + cards := map[string]*model.IotCard{ + "在线卡": {NetworkStatus: constants.NetworkStatusOnline, EnablePolling: false}, + "设备下在线卡": {NetworkStatus: constants.NetworkStatusOnline, EnablePolling: false, IsStandalone: false}, + "无套餐停机的卡": {NetworkStatus: constants.NetworkStatusOffline, StopReason: constants.StopReasonNoPackage, EnablePolling: false}, + } + sceneSets := map[string][]string{ + "无场景": nil, + "套餐到期": {constants.StopResumeScenePackageExpired}, + "退款": {constants.StopResumeSceneRefund}, + "流量扣减": {constants.StopResumeSceneUsage}, + } + for cardName, card := range cards { + for sceneName, scenes := range sceneSets { + t.Run(cardName+"/"+sceneName, func(t *testing.T) { + decision, err := svc.decideCard(context.Background(), card, scenes) + if err != nil { + t.Fatalf("期望 nil,实际 %v", err) + } + if decision.Action != "" { + t.Fatalf("关闭轮询的卡不应停复机,实际动作 %q", decision.Action) + } + }) + } + } +} + +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) + } +} diff --git a/internal/service/iot_card/stop_resume_service.go b/internal/service/iot_card/stop_resume_service.go index 15357acc..0bf4d924 100644 --- a/internal/service/iot_card/stop_resume_service.go +++ b/internal/service/iot_card/stop_resume_service.go @@ -134,6 +134,9 @@ func (s *StopResumeService) SetPollingCallback(callback PollingCallback) { // 停复机出口开关开启时只请求评估(生成指令或请求设备判断),不当场调用网关;关闭时当场执行。 // 设备/单卡维度通过 card.IsStandalone 推导(false 则为设备维度,停/复机覆盖设备下所有卡)。触发场景见 WithStopResumeScene。 func (s *StopResumeService) EvaluateAndAct(ctx context.Context, card *model.IotCard) error { + if card == nil { + return nil + } scenes := stopResumeScenes(ctx) decision, err := s.decideCard(ctx, card, scenes) if err != nil { @@ -376,7 +379,7 @@ func (s *StopResumeService) stopDeviceCards(ctx context.Context, deviceID uint, // 查询设备下在线卡;套餐失效类场景(套餐到期、退款)不看库里的开停状态(可能因上游延迟是旧值),设备下全部卡都停。 var cards []*model.IotCard var err error - if stopReason == constants.StopReasonNoPackage && constants.StopResumeSceneAllowsSuspendedStop(stopResumeScenes(ctx)) { + if stopReason == constants.StopReasonNoPackage && constants.StopResumeSceneIgnoresNetworkStatus(stopResumeScenes(ctx)) { cards, err = s.deviceCards(ctx, deviceID) } else { cards, err = s.iotCardStore.ListByDeviceIDAndNetworkStatus(ctx, deviceID, constants.NetworkStatusOnline) @@ -389,11 +392,11 @@ func (s *StopResumeService) stopDeviceCards(ctx context.Context, deviceID uint, var cardErrors []error for _, card := range cards { - // 暂停轮询的卡不随设备自动停机;套餐失效类场景(套餐到期、退款)照常停机。 + // 暂停轮询的卡已交由其他卡管,任何场景都不随设备自动停机。 if suspended, suspendErr := s.autoControlSuspended(ctx, card); suspendErr != nil { cardErrors = append(cardErrors, suspendErr) continue - } else if suspended && !(stopReason == constants.StopReasonNoPackage && constants.StopResumeSceneAllowsSuspendedStop(stopResumeScenes(ctx))) { + } else if suspended { continue } if _, stopErr := s.stopCardWithRetry(ctx, card, stopReason); stopErr != nil { diff --git a/pkg/constants/stop_resume_queue.go b/pkg/constants/stop_resume_queue.go index ff0ec3cf..02d0b051 100644 --- a/pkg/constants/stop_resume_queue.go +++ b/pkg/constants/stop_resume_queue.go @@ -22,8 +22,9 @@ const ( StopResumeSceneUsage = "usage" // 流量扣减后 ) -// StopResumeSceneAllowsSuspendedStop 判断场景集合中是否含套餐失效类场景:暂停轮询的卡在这些场景下照常停机。 -func StopResumeSceneAllowsSuspendedStop(scenes []string) bool { +// StopResumeSceneIgnoresNetworkStatus 判断场景集合中是否含套餐失效类场景(套餐到期、退款): +// 这些场景下停机判断不看库里的开停状态(上游状态有延迟可能是旧值)。暂停轮询的卡在任何场景下都不停机。 +func StopResumeSceneIgnoresNetworkStatus(scenes []string) bool { for _, scene := range scenes { if scene == StopResumeScenePackageExpired || scene == StopResumeSceneRefund { return true