fix: 卡或绑定设备关闭轮询时不再自动停复机
关闭轮询的卡/设备已导入其他卡管,去掉套餐到期、退款场景对暂停轮询卡的停机放行, 任何场景都不停机也不复机(单卡判断与设备下逐卡停机同步收口)。 场景判断函数更名为 StopResumeSceneIgnoresNetworkStatus,只保留不看库里开停状态的语义。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
48
internal/service/iot_card/stop_resume_polling_guard_test.go
Normal file
48
internal/service/iot_card/stop_resume_polling_guard_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user