diff --git a/internal/application/prioritypolling/event.go b/internal/application/prioritypolling/event.go index bf0b7000..206c5b01 100644 --- a/internal/application/prioritypolling/event.go +++ b/internal/application/prioritypolling/event.go @@ -48,9 +48,9 @@ type pollingPriorityTriggerKey struct{} // WithPollingPriorityTrigger 标记本次业务评估来自普通套餐轮询。 // -// 「资产无有效套餐」这一场景在停复机评估内部判定,而该评估入口被观测消费者、手动实名、保护期轮询、 +// 「资产无有效套餐」这一场景在停复机评估内部判定,而该评估入口被观测消费者、手动实名、 // 既有兼容入口等多条调用链复用;只有普通套餐轮询来源才允许追加优先轮询请求, -// 否则会在观测序列、手动实名、保护期与退款等上下文凭空产生加急触发。 +// 否则会在观测序列、手动实名与退款等上下文凭空产生加急触发。 func WithPollingPriorityTrigger(ctx context.Context) context.Context { return context.WithValue(ctx, pollingPriorityTriggerKey{}, true) } diff --git a/internal/bootstrap/services.go b/internal/bootstrap/services.go index 48eb4928..f8b5b267 100644 --- a/internal/bootstrap/services.go +++ b/internal/bootstrap/services.go @@ -271,7 +271,7 @@ func initServices(s *stores, deps *Dependencies) *services { if deps.GatewayLimit != nil { stopResumeService.SetStopResumeRecorder(deps.GatewayLimit.Recorder) } - // 持通道阈值锁的卡在周期内拒绝一切复机(自动、手动、保护期强制、机卡分离)。 + // 持通道阈值锁的卡在周期内拒绝一切复机(自动、手动、机卡分离)。 stopResumeService.SetChannelThresholdLockGuard(carrierThresholdService) iotCard.SetRealnameActivator(packageActivation) iotCard.SetStopResumeService(stopResumeService) diff --git a/internal/bootstrap/worker_services.go b/internal/bootstrap/worker_services.go index 5cb3475d..edf717f5 100644 --- a/internal/bootstrap/worker_services.go +++ b/internal/bootstrap/worker_services.go @@ -195,7 +195,7 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q if deps.GatewayLimit != nil { stopResumeService.SetStopResumeRecorder(deps.GatewayLimit.Recorder) } - // 持通道阈值锁的卡在周期内拒绝一切复机(自动、手动、保护期强制、机卡分离)。 + // 持通道阈值锁的卡在周期内拒绝一切复机(自动、手动、机卡分离)。 stopResumeService.SetChannelThresholdLockGuard(carrierThresholdService) // 停复机执行端口复用既有停复机服务作为唯一事实源:消费者与两个计划任务共用同一用例实例。 carrierThresholdService.SetCommander(carrierThresholdInfra.NewCardCommander(stopResumeService)) diff --git a/internal/handler/admin/polling_manual_trigger.go b/internal/handler/admin/polling_manual_trigger.go index 144c167f..916441cb 100644 --- a/internal/handler/admin/polling_manual_trigger.go +++ b/internal/handler/admin/polling_manual_trigger.go @@ -157,7 +157,6 @@ func (h *PollingManualTriggerHandler) GetStatus(c *fiber.Ctx) error { constants.TaskTypePollingRealname, constants.TaskTypePollingCarddata, constants.TaskTypePollingPackage, - constants.TaskTypePollingProtect, } { size, _ := h.service.GetQueueSize(ctx, taskType) queueSizes[taskType] = size @@ -272,6 +271,7 @@ func (h *PollingManualTriggerHandler) getTaskTypeName(taskType string) string { case constants.TaskTypePollingPackage: return "套餐检查" case constants.TaskTypePollingProtect: + // 保护期已下线,保留名称供历史手动触发记录展示。 return "保护期检查" default: return taskType diff --git a/internal/model/dto/asset_dto.go b/internal/model/dto/asset_dto.go index f87b2070..90771d96 100644 --- a/internal/model/dto/asset_dto.go +++ b/internal/model/dto/asset_dto.go @@ -35,7 +35,7 @@ type AssetResolveResponse struct { VirtualUsedMB float64 `json:"virtual_used_mb" description:"当前主套餐展示已用量(MB)"` ReductionPct float64 `json:"reduction_pct" description:"展示增幅比例,公式为(real_total_mb / virtual_total_mb) - 1"` EnableVirtualData bool `json:"enable_virtual_data" description:"当前主套餐是否启用虚流量(按套餐使用记录快照返回)"` - DeviceProtectStatus string `json:"device_protect_status,omitempty" description:"设备保护期状态:none/stop/start(仅asset_type=device时有效)"` + DeviceProtectStatus string `json:"device_protect_status,omitempty" deprecated:"true" description:"已废弃:设备保护期已下线,固定返回 none,仅为兼容保留(仅asset_type=device时有效)"` // 绑定关系字段 AssociatedPhones []string `json:"associated_phones" description:"当前关联手机号列表(按资产数据范围返回完整手机号,无关联时为空数组)"` ICCID string `json:"iccid,omitempty" description:"卡ICCID(asset_type=card时有效)"` @@ -141,7 +141,7 @@ type AssetRealtimeStatusResponse struct { LastDataCheckAt *time.Time `json:"last_data_check_at" description:"最后一次流量检查时间(asset_type=card时有效)"` LastRealNameCheckAt *time.Time `json:"last_real_name_check_at" description:"最后一次实名检查时间(asset_type=card时有效)"` LastCardStatusCheckAt *time.Time `json:"last_card_status_check_at" description:"最后一次卡状态检查时间(asset_type=card时有效)"` - DeviceProtectStatus string `json:"device_protect_status,omitempty" description:"保护期状态(asset_type=device时有效):none/stop/start"` + DeviceProtectStatus string `json:"device_protect_status,omitempty" deprecated:"true" description:"已废弃:设备保护期已下线,固定返回 none,仅为兼容保留(asset_type=device时有效)"` Cards []BoundCardInfo `json:"cards,omitempty" description:"绑定卡状态列表(asset_type=device时有效)"` DeviceRealtime *DeviceGatewayInfo `json:"device_realtime" description:"设备实时状态(来自 Gateway sync-info,始终返回,失败时仅含 gateway_msg)"` } diff --git a/internal/model/dto/client_asset_dto.go b/internal/model/dto/client_asset_dto.go index 1ad6c1d8..7c0bbf42 100644 --- a/internal/model/dto/client_asset_dto.go +++ b/internal/model/dto/client_asset_dto.go @@ -72,7 +72,7 @@ type AssetInfoResponse struct { MaxSimSlots int `json:"max_sim_slots,omitempty" description:"最大插槽数"` BoundCardCount int `json:"bound_card_count,omitempty" description:"绑定卡数量"` Cards []BoundCardInfo `json:"cards,omitempty" description:"绑定卡列表(含每张卡的ICCID/MSISDN/网络状态/实名状态/插槽位置)"` - DeviceProtectStatus string `json:"device_protect_status,omitempty" description:"设备保护期状态(none:无, stop:停机保护, start:开机保护)"` + DeviceProtectStatus string `json:"device_protect_status,omitempty" deprecated:"true" description:"已废弃:设备保护期已下线,固定返回 none,仅为兼容保留"` // === 卡专属字段(asset_type=card 时有效) === ICCID string `json:"iccid,omitempty" description:"卡ICCID"` diff --git a/internal/model/dto/polling_config_dto.go b/internal/model/dto/polling_config_dto.go index 3268ffb3..deeddce4 100644 --- a/internal/model/dto/polling_config_dto.go +++ b/internal/model/dto/polling_config_dto.go @@ -10,7 +10,6 @@ type CreatePollingConfigRequest struct { RealnameCheckInterval *int `json:"realname_check_interval" validate:"omitempty,min=0" minimum:"0" description:"实名检查间隔(秒):正数按间隔(最小30秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` CarddataCheckInterval *int `json:"carddata_check_interval" validate:"omitempty,min=0" minimum:"0" description:"流量检查间隔(秒):正数按间隔(最小60秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` PackageCheckInterval *int `json:"package_check_interval" validate:"omitempty,min=0" minimum:"0" description:"套餐检查间隔(秒):正数按间隔(最小60秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` - ProtectCheckInterval *int `json:"protect_check_interval" validate:"omitempty,min=30" minimum:"30" description:"保护期一致性检查间隔(秒),NULL表示不检查,最小30秒"` CardStatusCheckInterval *int `json:"card_status_check_interval" validate:"omitempty,min=0" minimum:"0" description:"卡状态检查间隔(秒):正数按间隔(最小30秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` PackageState *string `json:"package_state" validate:"omitempty,oneof=active depleted pending none" enum:"active,depleted,pending,none" description:"套餐状态匹配 (active:有生效套餐, depleted:套餐已用完, pending:只有待生效套餐, none:无套餐),为空不限"` AssetStatus *int `json:"asset_status" validate:"omitempty,oneof=1 2 3 4" enum:"1,2,3,4" description:"资产状态匹配 (1:在库, 2:已销售, 3:已换货, 4:已停用),为空不限"` @@ -29,7 +28,6 @@ type UpdatePollingConfigRequest struct { RealnameCheckInterval *int `json:"realname_check_interval" validate:"omitempty,min=0" minimum:"0" description:"实名检查间隔(秒):正数按间隔(最小30秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` CarddataCheckInterval *int `json:"carddata_check_interval" validate:"omitempty,min=0" minimum:"0" description:"流量检查间隔(秒):正数按间隔(最小60秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` PackageCheckInterval *int `json:"package_check_interval" validate:"omitempty,min=0" minimum:"0" description:"套餐检查间隔(秒):正数按间隔(最小60秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` - ProtectCheckInterval *int `json:"protect_check_interval" validate:"omitempty,min=30" minimum:"30" description:"保护期一致性检查间隔(秒),NULL表示不检查,最小30秒"` CardStatusCheckInterval *int `json:"card_status_check_interval" validate:"omitempty,min=0" minimum:"0" description:"卡状态检查间隔(秒):正数按间隔(最小30秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` PackageState *string `json:"package_state" validate:"omitempty,oneof=active depleted pending none" enum:"active,depleted,pending,none" description:"套餐状态匹配 (active:有生效套餐, depleted:套餐已用完, pending:只有待生效套餐, none:无套餐),为空不限"` AssetStatus *int `json:"asset_status" validate:"omitempty,oneof=1 2 3 4" enum:"1,2,3,4" description:"资产状态匹配 (1:在库, 2:已销售, 3:已换货, 4:已停用),为空不限"` @@ -66,7 +64,6 @@ type PollingConfigResponse struct { RealnameCheckInterval *int `json:"realname_check_interval" description:"实名检查间隔(秒):正数按间隔,0 不检查,空继承"` CarddataCheckInterval *int `json:"carddata_check_interval" description:"流量检查间隔(秒):正数按间隔,0 不检查,空继承"` PackageCheckInterval *int `json:"package_check_interval" description:"套餐检查间隔(秒):正数按间隔,0 不检查,空继承"` - ProtectCheckInterval *int `json:"protect_check_interval" description:"保护期一致性检查间隔(秒),NULL表示不检查"` CardStatusCheckInterval *int `json:"card_status_check_interval" description:"卡状态检查间隔(秒):正数按间隔,0 不检查,空继承"` PackageState *string `json:"package_state" description:"套餐状态匹配 (active:有生效套餐, depleted:套餐已用完, pending:只有待生效套餐, none:无套餐),为空不限"` AssetStatus *int `json:"asset_status" description:"资产状态匹配 (1:在库, 2:已销售, 3:已换货, 4:已停用),为空不限"` @@ -117,7 +114,6 @@ type PollingConfigOverride struct { RealnameCheckInterval *int `json:"realname_check_interval" minimum:"0" description:"实名检查间隔(秒),正数按间隔、0 不检查、空继承"` CarddataCheckInterval *int `json:"carddata_check_interval" minimum:"0" description:"流量检查间隔(秒),正数按间隔、0 不检查、空继承"` PackageCheckInterval *int `json:"package_check_interval" minimum:"0" description:"套餐检查间隔(秒),正数按间隔、0 不检查、空继承"` - ProtectCheckInterval *int `json:"protect_check_interval" minimum:"0" description:"保护期检查间隔(秒),正数按间隔、0 不检查、空继承"` CardStatusCheckInterval *int `json:"card_status_check_interval" minimum:"0" description:"卡状态检查间隔(秒),正数按间隔、0 不检查、空继承"` PackageState *string `json:"package_state" enum:"active,depleted,pending,none" description:"套餐状态匹配 (active:有生效套餐, depleted:套餐已用完, pending:只有待生效套餐, none:无套餐),为空不限"` AssetStatus *int `json:"asset_status" enum:"1,2,3,4" description:"资产状态匹配 (1:在库, 2:已销售, 3:已换货, 4:已停用),为空不限"` @@ -159,7 +155,7 @@ type PollingConfigBrief struct { // PollingIntervalPreview 某检查类型最终采用的间隔与来源 type PollingIntervalPreview struct { - TaskType string `json:"task_type" description:"检查类型 (polling:realname:实名, polling:carddata:流量, polling:package:套餐, polling:card_status:卡状态, polling:protect:保护期)"` + TaskType string `json:"task_type" description:"检查类型 (polling:realname:实名, polling:carddata:流量, polling:package:套餐, polling:card_status:卡状态)"` TaskTypeName string `json:"task_type_name" description:"检查类型名称"` Mode string `json:"mode" description:"结果 (interval:按间隔检查, disabled:不检查, unmatched:没有任何配置给出取值,不检查)"` Interval int `json:"interval" description:"间隔(秒),仅 mode=interval 时有值"` diff --git a/internal/model/iot_card.go b/internal/model/iot_card.go index e56d21c0..e75292fa 100644 --- a/internal/model/iot_card.go +++ b/internal/model/iot_card.go @@ -38,7 +38,6 @@ type IotCard struct { LastRealNameCheckAt *time.Time `gorm:"column:last_real_name_check_at;comment:最后一次实名检查时间" json:"last_real_name_check_at"` RealnameReversalCount int `gorm:"column:realname_reversal_count;type:int;default:0;not null;comment:实名逆转连续观测次数" json:"realname_reversal_count"` RealnameReversalStartedAt *time.Time `gorm:"column:realname_reversal_started_at;comment:实名逆转当前确认窗口开始时间" json:"realname_reversal_started_at,omitempty"` - LastProtectCheckAt *time.Time `gorm:"column:last_protect_check_at;comment:上次保护期一致性检查时间" json:"last_protect_check_at"` LastCardStatusCheckAt *time.Time `gorm:"column:last_card_status_check_at;comment:最后一次卡状态检查时间" json:"last_card_status_check_at"` LastSyncTime *time.Time `gorm:"column:last_sync_time;comment:最后一次与Gateway同步时间" json:"last_sync_time"` SeriesID *uint `gorm:"column:series_id;index;comment:套餐系列ID(关联PackageSeries)" json:"series_id,omitempty"` diff --git a/internal/model/polling.go b/internal/model/polling.go index 0e743888..08286c90 100644 --- a/internal/model/polling.go +++ b/internal/model/polling.go @@ -17,7 +17,6 @@ type PollingConfig struct { RealnameCheckInterval *int `gorm:"column:realname_check_interval;comment:实名检查间隔(秒):正数按间隔,0不检查(停止继承),NULL继承" json:"realname_check_interval"` CarddataCheckInterval *int `gorm:"column:carddata_check_interval;comment:流量检查间隔(秒):正数按间隔,0不检查(停止继承),NULL继承" json:"carddata_check_interval"` PackageCheckInterval *int `gorm:"column:package_check_interval;comment:套餐检查间隔(秒):正数按间隔,0不检查(停止继承),NULL继承" json:"package_check_interval"` - ProtectCheckInterval *int `gorm:"column:protect_check_interval;comment:保护期一致性检查间隔(秒),NULL=不参与" json:"protect_check_interval"` CardStatusCheckInterval *int `gorm:"column:card_status_check_interval;comment:卡状态检查间隔(秒):正数按间隔,0不检查(停止继承),NULL继承" json:"card_status_check_interval"` PackageState *string `gorm:"column:package_state;type:varchar(20);comment:套餐状态匹配:active/depleted/pending/none,NULL不限" json:"package_state"` AssetStatus *int `gorm:"column:asset_status;type:smallint;comment:资产状态匹配:1在库 2已销售 3已换货 4已停用,NULL不限" json:"asset_status"` diff --git a/internal/polling/config_manager.go b/internal/polling/config_manager.go index d97ef511..c242d7cc 100644 --- a/internal/polling/config_manager.go +++ b/internal/polling/config_manager.go @@ -200,7 +200,7 @@ func (m *PollingConfigManager) lazyTierFacts(card *model.IotCard) func() TierFac // hasAnyConfiguredInterval 检查配置是否至少配置了一种检查间隔(正数或 0)。 func hasAnyConfiguredInterval(cfg *model.PollingConfig) bool { return cfg.RealnameCheckInterval != nil || cfg.CarddataCheckInterval != nil || cfg.PackageCheckInterval != nil || - (cfg.ProtectCheckInterval != nil && *cfg.ProtectCheckInterval > 0) || cfg.CardStatusCheckInterval != nil + cfg.CardStatusCheckInterval != nil } // IntervalSource 是某检查类型最终采用的间隔及其来源,供合并结果与匹配预览共用。 @@ -213,15 +213,11 @@ type IntervalSource struct { Config *model.PollingConfig } -// taskIntervalFields 返回配置中各检查类型的间隔字段(保护期按既有语义只认正数)。 +// taskIntervalFields 返回配置中各检查类型的间隔字段。 func taskIntervalFields(cfg *model.PollingConfig) []struct { taskType string value *int } { - protect := cfg.ProtectCheckInterval - if protect != nil && *protect <= 0 { - protect = nil - } return []struct { taskType string value *int @@ -229,7 +225,6 @@ func taskIntervalFields(cfg *model.PollingConfig) []struct { {constants.TaskTypePollingRealname, cfg.RealnameCheckInterval}, {constants.TaskTypePollingCarddata, cfg.CarddataCheckInterval}, {constants.TaskTypePollingPackage, cfg.PackageCheckInterval}, - {constants.TaskTypePollingProtect, protect}, {constants.TaskTypePollingCardStatus, cfg.CardStatusCheckInterval}, } } diff --git a/internal/polling/initializer.go b/internal/polling/initializer.go index d90a9a68..d6a50cc2 100644 --- a/internal/polling/initializer.go +++ b/internal/polling/initializer.go @@ -327,8 +327,6 @@ func lastCheckAtByTaskType(card *model.IotCard, taskType string) *time.Time { return card.LastRealNameCheckAt case constants.TaskTypePollingCarddata, constants.TaskTypePollingPackage: return card.LastDataCheckAt - case constants.TaskTypePollingProtect: - return card.LastProtectCheckAt case constants.TaskTypePollingCardStatus: return card.LastCardStatusCheckAt default: diff --git a/internal/polling/queue_manager.go b/internal/polling/queue_manager.go index 4018efb8..8d65b511 100644 --- a/internal/polling/queue_manager.go +++ b/internal/polling/queue_manager.go @@ -17,7 +17,6 @@ var allTaskTypes = []string{ constants.TaskTypePollingRealname, constants.TaskTypePollingCarddata, constants.TaskTypePollingPackage, - constants.TaskTypePollingProtect, constants.TaskTypePollingCardStatus, } diff --git a/internal/service/asset/service.go b/internal/service/asset/service.go index 2cf3c507..4f8ff88d 100644 --- a/internal/service/asset/service.go +++ b/internal/service/asset/service.go @@ -295,8 +295,8 @@ func (s *Service) buildDeviceResolveResponse(ctx context.Context, device *model. // 查套餐系列名称 s.fillSeriesName(ctx, resp) - // 查 Redis 保护期 - resp.DeviceProtectStatus = s.getDeviceProtectStatus(ctx, device.ID) + // 保护期已下线,字段固定返回无保护期,兼容老版本客户端。 + resp.DeviceProtectStatus = constants.DeviceProtectStatusNone // 关联手机号:详情为单资产单次查询,按数据范围返回完整手机号。 phones, err := s.resolveAssociatedPhones(ctx, constants.AssetTypeDevice, device.ID) @@ -525,7 +525,7 @@ func (s *Service) GetRealtimeStatus(ctx context.Context, assetType string, id ui }) } } - resp.DeviceProtectStatus = s.getDeviceProtectStatus(ctx, id) + resp.DeviceProtectStatus = constants.DeviceProtectStatusNone // 实时查询 Gateway 设备状态(不缓存,per D-05) // Gateway 失败不阻断主流程,DeviceRealtime 始终非 nil,失败时仅填 GatewayMsg(per D-06) @@ -1035,19 +1035,6 @@ func (s *Service) UpdatePackageUsage(ctx context.Context, assetType string, asse return s.buildAssetPackageResponse(ctx, updated, constants.OwnerTypePlatform), nil } -// getDeviceProtectStatus 查询设备保护期状态 -func (s *Service) getDeviceProtectStatus(ctx context.Context, deviceID uint) string { - stopKey := constants.RedisDeviceProtectKey(deviceID, "stop") - startKey := constants.RedisDeviceProtectKey(deviceID, "start") - if s.redis.Exists(ctx, stopKey).Val() > 0 { - return "stop" - } - if s.redis.Exists(ctx, startKey).Val() > 0 { - return "start" - } - return "none" -} - // nonZeroTimePtr 将非零时间转为指针,零值返回 nil(避免序列化出历史时区偏移 +08:05) func nonZeroTimePtr(t time.Time) *time.Time { if t.IsZero() { diff --git a/internal/service/device/service.go b/internal/service/device/service.go index ac21f15b..978874db 100644 --- a/internal/service/device/service.go +++ b/internal/service/device/service.go @@ -1378,17 +1378,6 @@ func (s *Service) StopDevice(ctx context.Context, deviceID uint) (*dto.DeviceSus return nil, errors.New(errors.CodeNotFound, "设备不存在") } - // 复机保护期内禁止停机 - if s.redis != nil { - exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(deviceID, "start")).Result() - if exists > 0 { - appErr := errors.New(errors.CodeForbidden, "设备复机保护期内,禁止停机") - s.recordDeviceCommandAudit(ctx, constants.AuditActionDeviceStopped, "停用设备绑定卡网络被拒绝", - constants.AuditResultDenied, device, nil, nil, nil, nil, appErr) - return nil, appErr - } - } - bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, deviceID) if err != nil { appErr := errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败") @@ -1505,12 +1494,6 @@ func (s *Service) StopDevice(ctx context.Context, deviceID uint) (*dto.DeviceSus successCount++ } - // 成功停机至少一张卡后设置保护期 - if successCount > 0 && s.redis != nil { - s.redis.Set(ctx, constants.RedisDeviceProtectKey(deviceID, "stop"), uuid.NewString(), constants.DeviceProtectPeriodDuration) - s.redis.Del(ctx, constants.RedisDeviceProtectKey(deviceID, "start")) - } - if successCount == 0 && len(failedItems) == 0 { s.recordDeviceCommandAudit(ctx, constants.AuditActionDeviceStopped, "设备绑定卡网络无需停用", constants.AuditResultSuccess, device, nil, nil, nil, @@ -1541,17 +1524,6 @@ func (s *Service) StartDevice(ctx context.Context, deviceID uint) error { return errors.New(errors.CodeNotFound, "设备不存在") } - // 停机保护期内禁止复机 - if s.redis != nil { - exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(deviceID, "stop")).Result() - if exists > 0 { - appErr := errors.New(errors.CodeForbidden, "设备停机保护期内,禁止复机") - s.recordDeviceCommandAudit(ctx, constants.AuditActionDeviceStarted, "恢复设备绑定卡网络被拒绝", - constants.AuditResultDenied, device, nil, nil, nil, nil, appErr) - return appErr - } - } - bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, deviceID) if err != nil { appErr := errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败") @@ -1664,12 +1636,6 @@ func (s *Service) StartDevice(ctx context.Context, deviceID uint) error { successCount++ } - // 成功复机至少一张卡后设置保护期 - if successCount > 0 && s.redis != nil { - s.redis.Set(ctx, constants.RedisDeviceProtectKey(deviceID, "start"), uuid.NewString(), constants.DeviceProtectPeriodDuration) - s.redis.Del(ctx, constants.RedisDeviceProtectKey(deviceID, "stop")) - } - // 全部失败时返回 error if successCount == 0 && lastErr != nil { appErr := errors.Wrap(errors.CodeGatewayError, lastErr, "设备复机失败,所有卡均复机失败") diff --git a/internal/service/iot_card/stop_resume_service.go b/internal/service/iot_card/stop_resume_service.go index 28313b8d..4e8c5700 100644 --- a/internal/service/iot_card/stop_resume_service.go +++ b/internal/service/iot_card/stop_resume_service.go @@ -27,16 +27,10 @@ import ( ) // StopResumeServiceInterface 停复机服务接口 -// 供 4 个 Task Handler 通过接口注入,避免循环依赖 +// 供轮询 Task Handler 通过接口注入,避免循环依赖 type StopResumeServiceInterface interface { // EvaluateAndAct 停复机统一入口,根据卡的当前状态自动判断并执行停机或复机 EvaluateAndAct(ctx context.Context, card *model.IotCard) error - // ForceStopCard 强制停机单张卡,不执行正常停机条件判断。 - ForceStopCard(ctx context.Context, card *model.IotCard, stopReason string) error - // ForceStartCard 强制复机单张卡,不执行正常复机条件判断。 - ForceStartCard(ctx context.Context, card *model.IotCard) error - // ProtectCheckSkipsForRealname 判定保护期一致性检查是否因实名跳过该卡。 - ProtectCheckSkipsForRealname(ctx context.Context, card *model.IotCard) (bool, error) } // 编译时验证 StopResumeService 实现了 StopResumeServiceInterface @@ -220,7 +214,7 @@ func containsStopReason(reasons []string, target string) bool { // // 三个前置条件缺一不可: // 1. 已注入优先轮询 Writer; -// 2. 本次评估来自普通套餐轮询(观测消费者、手动实名、保护期轮询等调用链不打标,一律不写); +// 2. 本次评估来自普通套餐轮询(观测消费者、手动实名等调用链不打标,一律不写); // 3. 该载体存在待生效的套餐使用记录,否则每个普通套餐周期都重新入队会形成自激循环。 // // 事件资源为该载体对应的资产(独立卡为自身,绑定设备为设备),卡快照由 Writer 在同一事务内冻结。 @@ -547,53 +541,6 @@ func (s *StopResumeService) ResumeCardIfStopped(ctx context.Context, carrierType } } -// ForceStopCard 强制停机单张卡,不执行正常停机条件判断。 -func (s *StopResumeService) ForceStopCard(ctx context.Context, card *model.IotCard, stopReason string) error { - if card == nil || card.ID == 0 { - return errors.New(errors.CodeInvalidParam) - } - _, err := s.stopCardWithRetry(ctx, card, stopReason) - return err -} - -// ForceStartCard 强制复机单张卡,不执行正常复机条件判断。 -// 持通道阈值锁的卡必须先拒绝,再走既有保护期一致性检查,避免强行复机锁定期内的卡。 -func (s *StopResumeService) ForceStartCard(ctx context.Context, card *model.IotCard) error { - if card == nil || card.ID == 0 { - return errors.New(errors.CodeInvalidParam) - } - actionCode, summary := constants.AuditActionIotCardAutoStarted, "自动恢复 IoT 卡网络" - blocked, err := s.channelThresholdBlocked(ctx, card) - if err != nil { - return err - } - if blocked { - s.recordChannelThresholdDenial(ctx, card, actionCode, summary) - return channelThresholdDenyError() - } - attempt, integrationID, err := s.resumeCardWithRetry(ctx, card, actionCode, summary) - if err != nil { - return err - } - if err := s.updateCardAndAppendNetworkSeries(ctx, card, map[string]any{ - "network_status": constants.NetworkStatusOnline, - "resumed_at": time.Now(), - "stop_reason": "", - }, constants.CardObservationSceneBusinessResume, "online", uuid.NewString(), actionCode, summary, integrationID); err != nil { - if logErr := s.completeCardCommandAttempt(ctx, attempt, nil, false); logErr != nil { - s.logger.Error("终结保护期复机 Integration Log 失败", zap.String("integration_id", attempt.log.IntegrationID), zap.Error(logErr)) - } - s.recordCardCommandAudit(ctx, card, actionCode, summary+"结果待核对", constants.AuditResultUnknown, - attempt.log.IntegrationID, cardSnapshot(card), map[string]any{"requested_network_status": constants.NetworkStatusOnline}, err) - return err - } - s.reschedulePolling(ctx, card.ID) - if err := s.completeCardCommandAttempt(ctx, attempt, nil, true); err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "终结保护期复机 Integration Log 失败") - } - return nil -} - // resumeSingleCard 对单张卡执行复机逻辑 // 依次检查:持通道阈值锁则拒绝 → 已开机则跳过 → 非轮询停机原因则跳过 → 不满足复机条件则跳过 → 加锁 → 调 Gateway → 更新 DB func (s *StopResumeService) resumeSingleCard(ctx context.Context, cardID uint) error { @@ -615,7 +562,7 @@ func (s *StopResumeService) resumeSingleCard(ctx context.Context, cardID uint) e // 持通道阈值锁:周期内拒绝一切复机,记录拒绝事实并保留锁。 // 该入口被轮询评估、套餐激活/重置与设备维度复机复用,返回 nil 表示本次不动作, - // 避免让自动链路按失败重试;用户可见入口由 ManualStartCard/ForceStartCard 返回拒绝错误。 + // 避免让自动链路按失败重试;用户可见入口由 ManualStartCard 返回拒绝错误。 blocked, blockErr := s.channelThresholdBlocked(ctx, card) if blockErr != nil { return blockErr @@ -982,24 +929,6 @@ func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) er return denyErr } - // 检查绑定设备是否在复机保护期 - if s.deviceSimBindingStore != nil && s.redis != nil { - binding, bindErr := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID) - if bindErr == nil && binding != nil { - exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(binding.DeviceID, "start")).Result() - if exists > 0 { - denyErr := errors.New(errors.CodeForbidden, "设备复机保护期内,禁止停机") - s.recordCardCommandAudit(ctx, card, actionCode, summary+"被拒绝", constants.AuditResultDenied, "", - cardSnapshot(card), map[string]any{"device_id": binding.DeviceID}, denyErr) - return denyErr - } - } else if bindErr != nil && !stderrors.Is(bindErr, gorm.ErrRecordNotFound) { - wrapErr := errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败") - s.recordCardCommandAudit(ctx, card, actionCode, summary+"失败", constants.AuditResultFailed, "", cardSnapshot(card), nil, wrapErr) - return wrapErr - } - } - if _, err := s.stopCardWithRetry(ctx, card, constants.StopReasonManual); err != nil { return err } @@ -1049,24 +978,6 @@ func (s *StopResumeService) ManualStartCard(ctx context.Context, iccid string) e return denyErr } - // 检查绑定设备是否在停机保护期 - if s.deviceSimBindingStore != nil && s.redis != nil { - binding, bindErr := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID) - if bindErr == nil && binding != nil { - exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(binding.DeviceID, "stop")).Result() - if exists > 0 { - denyErr := errors.New(errors.CodeForbidden, "设备停机保护期内,禁止复机") - s.recordCardCommandAudit(ctx, card, actionCode, summary+"被拒绝", constants.AuditResultDenied, "", - cardSnapshot(card), map[string]any{"device_id": binding.DeviceID}, denyErr) - return denyErr - } - } else if bindErr != nil && !stderrors.Is(bindErr, gorm.ErrRecordNotFound) { - wrapErr := errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败") - s.recordCardCommandAudit(ctx, card, actionCode, summary+"失败", constants.AuditResultFailed, "", cardSnapshot(card), nil, wrapErr) - return wrapErr - } - } - attempt, integrationID, err := s.resumeCardWithRetry(ctx, card, actionCode, summary) if err != nil { wrapErr := errors.Wrap(errors.CodeGatewayError, err, "调用运营商复机失败,请稍后重试") diff --git a/internal/service/iot_card/stop_resume_unified.go b/internal/service/iot_card/stop_resume_unified.go index bb4bd117..1ff676ba 100644 --- a/internal/service/iot_card/stop_resume_unified.go +++ b/internal/service/iot_card/stop_resume_unified.go @@ -80,19 +80,3 @@ func (s *StopResumeService) unifiedRealnameOK(ctx context.Context, card *model.I } return card.RealNameStatus == constants.RealNameStatusVerified, nil } - -// ProtectCheckSkipsForRealname 判定保护期一致性检查是否因实名跳过该卡。 -// 总开关关闭时与原有行为一致(未实名即跳过);开启时只在通道开启实名管控且卡未实名时跳过。 -func (s *StopResumeService) ProtectCheckSkipsForRealname(ctx context.Context, card *model.IotCard) (bool, error) { - if card.RealNameStatus == constants.RealNameStatusVerified { - return false, nil - } - if !unifiedStopResumeEnabled() { - return true, nil - } - controlled, err := s.channelRealnameControlled(ctx, card) - if err != nil { - return false, err - } - return controlled, nil -} diff --git a/internal/service/polling/audit.go b/internal/service/polling/audit.go index c9f30d37..06770b8c 100644 --- a/internal/service/polling/audit.go +++ b/internal/service/polling/audit.go @@ -71,7 +71,7 @@ func pollingConfigState(config *model.PollingConfig) map[string]any { "config_name": config.ConfigName, "card_condition": config.CardCondition, "card_category": config.CardCategory, "carrier_id": config.CarrierID, "priority": config.Priority, "realname_check_interval": config.RealnameCheckInterval, "carddata_check_interval": config.CarddataCheckInterval, - "package_check_interval": config.PackageCheckInterval, "protect_check_interval": config.ProtectCheckInterval, + "package_check_interval": config.PackageCheckInterval, "card_status_check_interval": config.CardStatusCheckInterval, "status": config.Status, "package_state": config.PackageState, "asset_status": config.AssetStatus, "pre_reset_window": config.PreResetWindow, "pre_reset_carddata_interval": config.PreResetCarddataInterval, diff --git a/internal/service/polling/concurrency_service.go b/internal/service/polling/concurrency_service.go index 6188d825..99cd2dd4 100644 --- a/internal/service/polling/concurrency_service.go +++ b/internal/service/polling/concurrency_service.go @@ -58,6 +58,9 @@ func (s *ConcurrencyService) List(ctx context.Context) ([]*ConcurrencyStatus, er result := make([]*ConcurrencyStatus, 0, len(configs)) for _, cfg := range configs { + if isRetiredConcurrencyTaskType(cfg.TaskType) { + continue + } status := &ConcurrencyStatus{ TaskType: cfg.TaskType, TaskTypeName: s.getTaskTypeName(cfg.TaskType), @@ -91,6 +94,9 @@ func (s *ConcurrencyService) List(ctx context.Context) ([]*ConcurrencyStatus, er // GetByTaskType 根据任务类型获取并发配置及状态 func (s *ConcurrencyService) GetByTaskType(ctx context.Context, taskType string) (*ConcurrencyStatus, error) { + if isRetiredConcurrencyTaskType(taskType) { + return nil, retiredConcurrencyTaskTypeError() + } cfg, err := s.store.GetByTaskType(ctx, taskType) if err != nil { return nil, errors.Wrap(errors.CodeNotFound, err, "并发配置不存在") @@ -130,6 +136,9 @@ func (s *ConcurrencyService) UpdateMaxConcurrency(ctx context.Context, taskType if maxConcurrency < 1 || maxConcurrency > constants.PollingMaxConcurrencyLimit { return errors.New(errors.CodeInvalidParam, "并发数必须为 1-1000") } + if isRetiredConcurrencyTaskType(taskType) { + return retiredConcurrencyTaskTypeError() + } // 验证任务类型存在 config, err := s.store.GetByTaskType(ctx, taskType) @@ -177,6 +186,9 @@ func (s *ConcurrencyService) ResetConcurrency(ctx context.Context, taskType stri if operatorID == 0 { return errors.New(errors.CodeUnauthorized, "未授权访问") } + if isRetiredConcurrencyTaskType(taskType) { + return retiredConcurrencyTaskTypeError() + } // 验证任务类型存在 config, err := s.store.GetByTaskType(ctx, taskType) if err != nil { @@ -249,6 +261,9 @@ func (s *ConcurrencyService) InitFromDB(ctx context.Context) error { } for _, cfg := range configs { + if isRetiredConcurrencyTaskType(cfg.TaskType) { + continue + } configKey := constants.RedisPollingConcurrencyConfigKey(cfg.TaskType) if err := s.redis.Set(ctx, configKey, cfg.MaxConcurrency, 24*time.Hour).Err(); err != nil { // 忽略单个配置同步失败 @@ -256,6 +271,7 @@ func (s *ConcurrencyService) InitFromDB(ctx context.Context) error { } } _ = s.redis.Del(ctx, constants.RedisPollingConcurrencyConfigKey("stop_start")).Err() + _ = s.redis.Del(ctx, constants.RedisPollingConcurrencyConfigKey(retiredProtectConcurrencyTaskType)).Err() return nil } @@ -283,11 +299,21 @@ func (s *ConcurrencyService) getTaskTypeName(taskType string) string { return "流量检查" case "package": return "套餐检查" - case "protect": - return "保护期检查" case "card_status": return "卡状态检查" default: return taskType } } + +// retiredProtectConcurrencyTaskType 是已下线的保护期检查并发配置;数据库行保留但不再返回或维护。 +const retiredProtectConcurrencyTaskType = "protect" + +// isRetiredConcurrencyTaskType 判断并发配置是否属于已下线的轮询任务(兼容带 polling: 前缀的写法)。 +func isRetiredConcurrencyTaskType(taskType string) bool { + return strings.TrimPrefix(strings.TrimSpace(taskType), "polling:") == retiredProtectConcurrencyTaskType +} + +func retiredConcurrencyTaskTypeError() error { + return errors.New(errors.CodeInvalidParam, "保护期检查已下线,不再维护并发配置") +} diff --git a/internal/service/polling/config_service.go b/internal/service/polling/config_service.go index 1b219c3c..41cdf34e 100644 --- a/internal/service/polling/config_service.go +++ b/internal/service/polling/config_service.go @@ -79,7 +79,6 @@ func (s *ConfigService) Create(ctx context.Context, req *dto.CreatePollingConfig RealnameCheckInterval: req.RealnameCheckInterval, CarddataCheckInterval: req.CarddataCheckInterval, PackageCheckInterval: req.PackageCheckInterval, - ProtectCheckInterval: req.ProtectCheckInterval, CardStatusCheckInterval: req.CardStatusCheckInterval, PackageState: req.PackageState, AssetStatus: req.AssetStatus, @@ -188,9 +187,6 @@ func (s *ConfigService) Update(ctx context.Context, id uint, req *dto.UpdatePoll if req.PackageCheckInterval != nil { config.PackageCheckInterval = req.PackageCheckInterval } - if req.ProtectCheckInterval != nil { - config.ProtectCheckInterval = req.ProtectCheckInterval - } if req.CardStatusCheckInterval != nil { config.CardStatusCheckInterval = req.CardStatusCheckInterval } @@ -402,7 +398,6 @@ func (s *ConfigService) toResponse(c *model.PollingConfig) *dto.PollingConfigRes RealnameCheckInterval: c.RealnameCheckInterval, CarddataCheckInterval: c.CarddataCheckInterval, PackageCheckInterval: c.PackageCheckInterval, - ProtectCheckInterval: c.ProtectCheckInterval, CardStatusCheckInterval: c.CardStatusCheckInterval, PackageState: c.PackageState, AssetStatus: c.AssetStatus, diff --git a/internal/service/polling/config_tier_service.go b/internal/service/polling/config_tier_service.go index 7cc53ff3..eff9c735 100644 --- a/internal/service/polling/config_tier_service.go +++ b/internal/service/polling/config_tier_service.go @@ -149,7 +149,7 @@ func (s *ConfigService) MatchPreview(ctx context.Context, req *dto.PollingConfig return resp, nil } -var previewTaskTypes = []string{constants.TaskTypePollingCardStatus, constants.TaskTypePollingCarddata, constants.TaskTypePollingRealname, constants.TaskTypePollingPackage, constants.TaskTypePollingProtect} +var previewTaskTypes = []string{constants.TaskTypePollingCardStatus, constants.TaskTypePollingCarddata, constants.TaskTypePollingRealname, constants.TaskTypePollingPackage} // applyPreviewOverrides 把未保存修改合并进启用配置快照并重新排序;返回草稿配置标记。 func applyPreviewOverrides(configs []*model.PollingConfig, overrides []dto.PollingConfigOverride) ([]*model.PollingConfig, map[*model.PollingConfig]bool, error) { @@ -176,8 +176,8 @@ func applyPreviewOverrides(configs []*model.PollingConfig, overrides []dto.Polli ID: item.ID, ConfigName: item.ConfigName, CardCondition: item.CardCondition, CardCategory: item.CardCategory, CarrierID: item.CarrierID, Priority: item.Priority, RealnameCheckInterval: item.RealnameCheckInterval, CarddataCheckInterval: item.CarddataCheckInterval, PackageCheckInterval: item.PackageCheckInterval, - ProtectCheckInterval: item.ProtectCheckInterval, CardStatusCheckInterval: item.CardStatusCheckInterval, - PackageState: item.PackageState, AssetStatus: item.AssetStatus, PreResetWindow: item.PreResetWindow, + CardStatusCheckInterval: item.CardStatusCheckInterval, + PackageState: item.PackageState, AssetStatus: item.AssetStatus, PreResetWindow: item.PreResetWindow, PreResetCarddataInterval: item.PreResetCarddataInterval, Status: 1, } if draft.ConfigName == "" { @@ -336,11 +336,8 @@ func (s *ConfigService) TierStats(ctx context.Context) (*dto.PollingTierStatsRes return resp, nil } -// previewTaskTypeName 返回检查类型中文名称(保护期检查在优先轮询名称表之外单独处理)。 +// previewTaskTypeName 返回检查类型中文名称。 func previewTaskTypeName(taskType string) string { - if taskType == constants.TaskTypePollingProtect { - return "保护期检查" - } return constants.PollingPriorityTaskTypeName(taskType) } diff --git a/internal/service/polling/config_validation.go b/internal/service/polling/config_validation.go index cc1e97b1..2a4b9da9 100644 --- a/internal/service/polling/config_validation.go +++ b/internal/service/polling/config_validation.go @@ -22,7 +22,6 @@ func validatePollingConfig(cfg *model.PollingConfig) error { {"实名检查间隔", cfg.RealnameCheckInterval, 30}, {"流量检查间隔", cfg.CarddataCheckInterval, 60}, {"套餐检查间隔", cfg.PackageCheckInterval, 60}, - {"保护期一致性检查间隔", cfg.ProtectCheckInterval, 30}, {"卡状态检查间隔", cfg.CardStatusCheckInterval, 30}, } configured := false diff --git a/internal/service/polling/monitoring_service.go b/internal/service/polling/monitoring_service.go index c16e6811..6daa0833 100644 --- a/internal/service/polling/monitoring_service.go +++ b/internal/service/polling/monitoring_service.go @@ -265,8 +265,6 @@ func (s *MonitoringService) getTaskTypeName(taskType string) string { return "流量检查" case constants.TaskTypePollingPackage: return "套餐检查" - case constants.TaskTypePollingProtect: - return "保护期检查" case constants.TaskTypePollingCardStatus: return "卡状态检查" default: @@ -280,7 +278,6 @@ func pollingMonitorTaskTypes() []string { constants.TaskTypePollingRealname, constants.TaskTypePollingCarddata, constants.TaskTypePollingPackage, - constants.TaskTypePollingProtect, constants.TaskTypePollingCardStatus, } } diff --git a/internal/task/polling_package_handler.go b/internal/task/polling_package_handler.go index 54b76d75..00d67e01 100644 --- a/internal/task/polling_package_handler.go +++ b/internal/task/polling_package_handler.go @@ -112,7 +112,7 @@ func (h *PollingPackageHandler) Handle(ctx context.Context, t *asynq.Task) error zap.String("stop_reason", freshCard.StopReason)) } // 标记本次评估来自普通套餐轮询:无有效套餐场景据此才允许追加优先轮询请求, - // 其余调用链(观测消费者、手动实名、保护期轮询等)不打标即不写。 + // 其余调用链(观测消费者、手动实名等)不打标即不写。 evaluationCtx := priorityapp.WithPollingPriorityTrigger(ctx) if evalErr := h.stopResumeSvc.EvaluateAndAct(evaluationCtx, freshCard); evalErr != nil { h.base.logger.Warn("套餐检查后停复机评估失败", diff --git a/internal/task/polling_protect_handler.go b/internal/task/polling_protect_handler.go deleted file mode 100644 index 520e6fa0..00000000 --- a/internal/task/polling_protect_handler.go +++ /dev/null @@ -1,166 +0,0 @@ -package task - -import ( - "context" - "time" - - "github.com/hibiken/asynq" - "go.uber.org/zap" - "gorm.io/gorm" - - cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation" - "github.com/break/junhong_cmp_fiber/internal/gateway" - iot_card_svc "github.com/break/junhong_cmp_fiber/internal/service/iot_card" - "github.com/break/junhong_cmp_fiber/internal/store/postgres" - "github.com/break/junhong_cmp_fiber/pkg/constants" -) - -// PollingProtectHandler 保护期一致性检查任务处理器 -// 保护期内:直接调 Gateway 强制停/复机(绕过 EvaluateAndAct 三条件判断) -// 保护期结束:调 EvaluateAndAct 重新评估正常停复机条件 -// 两种路径不可混淆:保护期内=强制修正;保护期结束=重新评估 -type PollingProtectHandler struct { - base *PollingBase - iotCardStore *postgres.IotCardStore - deviceSimBindingStore *postgres.DeviceSimBindingStore - stopResumeSvc iot_card_svc.StopResumeServiceInterface -} - -// NewPollingProtectHandler 创建保护期一致性检查任务处理器 -func NewPollingProtectHandler( - db *gorm.DB, - observationSeriesEvents cardObservationApp.SeriesEventWriter, - base *PollingBase, - gw *gateway.Client, - iotCardStore *postgres.IotCardStore, - deviceSimBindingStore *postgres.DeviceSimBindingStore, - stopResumeSvc iot_card_svc.StopResumeServiceInterface, -) *PollingProtectHandler { - return &PollingProtectHandler{ - base: base, - iotCardStore: iotCardStore, - deviceSimBindingStore: deviceSimBindingStore, - stopResumeSvc: stopResumeSvc, - } -} - -// Handle 处理保护期一致性检查任务 -func (h *PollingProtectHandler) Handle(ctx context.Context, t *asynq.Task) error { - startTime := time.Now() - - cardID, ok := parseTaskPayload(t.Payload(), h.base.logger) - if !ok { - return nil - } - ctx = withPollingWorkerAuditContext(ctx, constants.TaskTypePollingProtect, "保护期一致性轮询任务", t.ResultWriter().TaskID()) - - if !h.base.acquireConcurrency(ctx, constants.TaskTypePollingProtect) { - h.base.logger.Debug("并发已满,重新入队", zap.Uint("card_id", cardID)) - return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect) - } - defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingProtect) - - card, err := h.iotCardStore.GetByID(ctx, cardID) - if err != nil { - if isNotFound(err) { - return nil - } - h.base.logger.Error("查询卡信息失败", zap.Uint("card_id", cardID), zap.Error(err)) - return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect) - } - - // 独立卡(未绑设备)跳过保护期检查 - if card.IsStandalone { - return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect) - } - - // 实名不满足的卡跳过保护期检查:停复机新规则总开关关闭时未实名即跳过,开启时只在通道开启实名管控且未实名时跳过 - if h.stopResumeSvc != nil { - skip, skipErr := h.stopResumeSvc.ProtectCheckSkipsForRealname(ctx, card) - if skipErr != nil { - h.base.logger.Warn("保护期检查判断实名管控失败,本次跳过", zap.Uint("card_id", cardID), zap.Error(skipErr)) - return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect) - } - if skip { - return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect) - } - } else if card.RealNameStatus != constants.RealNameStatusVerified { - return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect) - } - - binding, err := h.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID) - if err != nil || binding == nil { - return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect) - } - deviceID := binding.DeviceID - - stopProtectGeneration := h.base.redis.Get(ctx, constants.RedisDeviceProtectKey(deviceID, "stop")).Val() - startProtectGeneration := h.base.redis.Get(ctx, constants.RedisDeviceProtectKey(deviceID, "start")).Val() - stopProtect := stopProtectGeneration != "" - startProtect := startProtectGeneration != "" - - actionTaken := "no_action" - - switch { - case stopProtect && card.NetworkStatus == constants.NetworkStatusOnline: - // 保护期内:停机保护期发现开机卡 → 强制停机(绕过 EvaluateAndAct) - h.base.logger.Info("保护期一致性:停机保护期内发现开机卡,强制停机", - zap.Uint("card_id", card.ID), zap.Uint("device_id", deviceID)) - if h.stopResumeSvc == nil { - break - } - if err := h.stopResumeSvc.ForceStopCard(ctx, card, constants.StopReasonProtectPeriod); err != nil { - h.base.logger.Error("保护期强制停机失败", - zap.Uint("card_id", card.ID), zap.Error(err)) - h.base.updateStats(ctx, constants.TaskTypePollingProtect, false, time.Since(startTime)) - return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect) - } - h.base.updateCardCache(ctx, cardID, map[string]any{"network_status": constants.NetworkStatusOffline}) - actionTaken = "forced_stop" - - case startProtect && card.NetworkStatus == constants.NetworkStatusOffline: - // 保护期内:复机保护期发现停机卡 → 强制复机(绕过 EvaluateAndAct) - h.base.logger.Info("保护期一致性:复机保护期内发现停机卡,强制复机", - zap.Uint("card_id", card.ID), zap.Uint("device_id", deviceID)) - if h.stopResumeSvc == nil { - break - } - if err := h.stopResumeSvc.ForceStartCard(ctx, card); err != nil { - h.base.logger.Error("保护期强制复机失败", - zap.Uint("card_id", card.ID), zap.Error(err)) - h.base.updateStats(ctx, constants.TaskTypePollingProtect, false, time.Since(startTime)) - return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect) - } - h.base.updateCardCache(ctx, cardID, map[string]any{"network_status": constants.NetworkStatusOnline}) - actionTaken = "forced_resume" - - case !stopProtect && !startProtect: - // 保护期结束:调 EvaluateAndAct 重新评估正常停复机条件 - if h.stopResumeSvc != nil { - if evalErr := h.stopResumeSvc.EvaluateAndAct(ctx, card); evalErr != nil { - h.base.logger.Warn("保护期结束后停复机评估失败", - zap.Uint("card_id", cardID), zap.Error(evalErr)) - } - } - actionTaken = "evaluate" - } - - if h.base.verboseLog { - h.base.logger.Info("保护期检查详情", - zap.Uint("card_id", cardID), - zap.String("iccid", card.ICCID), - zap.Bool("stop_protect", stopProtect), - zap.Bool("start_protect", startProtect), - zap.Int("network_status_at_check", card.NetworkStatus), - zap.String("action_taken", actionTaken)) - } - - if updateErr := h.iotCardStore.UpdateFields(ctx, cardID, map[string]any{ - "last_protect_check_at": time.Now(), - }); updateErr != nil { - h.base.logger.Warn("更新保护期检查时间失败", zap.Uint("card_id", cardID), zap.Error(updateErr)) - } - - h.base.updateStats(ctx, constants.TaskTypePollingProtect, true, time.Since(startTime)) - return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect) -} diff --git a/migrations/000249_deprecate_device_protect_columns.down.sql b/migrations/000249_deprecate_device_protect_columns.down.sql new file mode 100644 index 00000000..a1cf8f21 --- /dev/null +++ b/migrations/000249_deprecate_device_protect_columns.down.sql @@ -0,0 +1,3 @@ +-- 回滚:恢复两列原注释。 +COMMENT ON COLUMN tb_polling_config.protect_check_interval IS '保护期一致性检查间隔(秒),NULL=不参与'; +COMMENT ON COLUMN tb_iot_card.last_protect_check_at IS '上次保护期一致性检查时间'; diff --git a/migrations/000249_deprecate_device_protect_columns.up.sql b/migrations/000249_deprecate_device_protect_columns.up.sql new file mode 100644 index 00000000..c0e97e8c --- /dev/null +++ b/migrations/000249_deprecate_device_protect_columns.up.sql @@ -0,0 +1,8 @@ +-- 下线设备保护期(remove-device-protect-period)。 +-- 背景:后台设备停复机设置 1 小时保护期,保护期轮询(polling:protect)方向不一致时强制停复机; +-- 自动停复机不看保护期,导致保护期内反复翻转。保护期整体下线,代码不再读写以下两列。 +-- +-- 设计选择:数据库只加不删,列保留,仅把注释标记为已废弃;不改数据。 + +COMMENT ON COLUMN tb_polling_config.protect_check_interval IS '已废弃(设备保护期已下线,不再读写):原保护期一致性检查间隔(秒)'; +COMMENT ON COLUMN tb_iot_card.last_protect_check_at IS '已废弃(设备保护期已下线,不再读写):原上次保护期一致性检查时间'; diff --git a/openspec/changes/remove-device-protect-period/tasks.md b/openspec/changes/remove-device-protect-period/tasks.md index e565408e..10bcc383 100644 --- a/openspec/changes/remove-device-protect-period/tasks.md +++ b/openspec/changes/remove-device-protect-period/tasks.md @@ -1,29 +1,29 @@ ## 1. 迁移 -- [ ] 1.1 新增成对迁移:更新 `tb_polling_config.protect_check_interval`、`tb_iot_card.last_protect_check_at` 列注释为"已废弃";down 恢复原注释。 +- [x] 1.1 新增成对迁移:更新 `tb_polling_config.protect_check_interval`、`tb_iot_card.last_protect_check_at` 列注释为"已废弃";down 恢复原注释。(`000249_deprecate_device_protect_columns`) ## 2. 设备与单卡停复机 -- [ ] 2.1 `internal/service/device/service.go`:设备停机、复机不再设置保护期,不再因保护期拒绝相反操作。 -- [ ] 2.2 `internal/service/iot_card/stop_resume_service.go`:手动停复机单卡(第 969、1036 行附近)去掉保护期检查。 -- [ ] 2.3 删除 `RedisDeviceProtectKey`、`DeviceProtectPeriodDuration`;保留 `StopReasonProtectPeriod` 并在注释说明不再产生新值。 +- [x] 2.1 `internal/service/device/service.go`:设备停机、复机不再设置保护期,不再因保护期拒绝相反操作。 +- [x] 2.2 `internal/service/iot_card/stop_resume_service.go`:手动停复机单卡(第 969、1036 行附近)去掉保护期检查。(同时删除只服务于保护期轮询的 `ForceStopCard`、`ForceStartCard`) +- [x] 2.3 删除 `RedisDeviceProtectKey`、`DeviceProtectPeriodDuration`;保留 `StopReasonProtectPeriod` 并在注释说明不再产生新值。 ## 3. 下线保护期轮询 -- [ ] 3.1 删除 `internal/task/polling_protect_handler.go`;`pkg/queue/handler.go` 对 `polling:protect` 改为空处理函数直接成功(下一次发布再移除)。 -- [ ] 3.2 调度器、初始化器、生命周期、监控与统计去掉 `protect` 类型;删除 `ProtectCheckSkipsForRealname`。 -- [ ] 3.3 轮询并发配置与任务类型名称去掉 `protect`,提交 `protect` 时拒绝。 -- [ ] 3.4 `PollingConfig` 模型与 DTO 去掉 `ProtectCheckInterval`;`config_manager.go` 匹配去掉保护期;`IotCard` 模型去掉 `LastProtectCheckAt` 的读写。 +- [x] 3.1 删除 `internal/task/polling_protect_handler.go`;`pkg/queue/handler.go` 对 `polling:protect` 改为空处理函数直接成功(下一次发布再移除)。(`TaskTypePollingProtect` 常量与队列权重暂留,用于消化存量任务) +- [x] 3.2 调度器、初始化器、生命周期、监控与统计去掉 `protect` 类型;删除 `ProtectCheckSkipsForRealname`。 +- [x] 3.3 轮询并发配置与任务类型名称去掉 `protect`,提交 `protect` 时拒绝。(数据库 `protect` 行保留,服务层过滤:列表不返回,查询、更新、重置返回参数错误) +- [x] 3.4 `PollingConfig` 模型与 DTO 去掉 `ProtectCheckInterval`;`config_manager.go` 匹配去掉保护期;`IotCard` 模型去掉 `LastProtectCheckAt` 的读写。 ## 4. 资产接口兼容 -- [ ] 4.1 后台与 C 端资产详情的 `device_protect_status` 固定返回 `none`,DTO 说明标记已废弃;`internal/service/asset/service.go` 不再读取保护期 key。 -- [ ] 4.2 重新生成接口文档。 +- [x] 4.1 后台与 C 端资产详情的 `device_protect_status` 固定返回 `none`,DTO 说明标记已废弃;`internal/service/asset/service.go` 不再读取保护期 key。(三个字段加 `deprecated:"true"`,接口文档显示已废弃) +- [x] 4.2 重新生成接口文档。 ## 5. 编译与规范校验 -- [ ] 5.1 `gofmt -w`;`go build ./cmd/api ./cmd/worker` 通过;全仓检索无残留的保护期 key 读写。 -- [ ] 5.2 `openspec validate remove-device-protect-period` 与 `openspec doctor --json` 通过。 +- [x] 5.1 `gofmt -w`;`go build ./cmd/api ./cmd/worker` 通过;全仓检索无残留的保护期 key 读写。 +- [x] 5.2 `openspec validate remove-device-protect-period` 与 `openspec doctor --json` 通过。(validate 通过;当前 openspec 无 doctor 命令) ## 6. 测试环境验证(自动化测试为 N/A,按 ENG-TEST-001) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 447ec56d..7c80f4c3 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -64,7 +64,7 @@ const ( TaskTypePollingRealname = "polling:realname" // 实名状态检查 TaskTypePollingCarddata = "polling:carddata" // 卡流量检查 TaskTypePollingPackage = "polling:package" // 套餐流量检查 - TaskTypePollingProtect = "polling:protect" // 保护期一致性检查 + TaskTypePollingProtect = "polling:protect" // 已下线:保护期一致性检查,仅保留空处理消化存量任务,下一次发布移除 TaskTypePollingCardStatus = "polling:card_status" // 卡开停机状态检查 // 套餐激活任务类型 @@ -235,7 +235,7 @@ const ( QueuePollingRealname = TaskTypePollingRealname // 实名轮询任务队列 QueuePollingCarddata = TaskTypePollingCarddata // 流量轮询任务队列 QueuePollingPackage = TaskTypePollingPackage // 套餐轮询任务队列 - QueuePollingProtect = TaskTypePollingProtect // 保护期轮询任务队列 + QueuePollingProtect = TaskTypePollingProtect // 已下线:保护期轮询任务队列,仅消化存量任务 QueuePollingCardStatus = TaskTypePollingCardStatus // 卡状态轮询任务队列 QueuePackageFirstActivation = TaskTypePackageFirstActivation // 首次实名激活任务队列 QueuePackageQueueActivation = TaskTypePackageQueueActivation // 主套餐排队激活任务队列 @@ -499,9 +499,11 @@ const ( AutoPurchaseStatusFailed = "failed" // 失败 ) -// 设备保护期相关时长常量 +// DeviceProtectStatusNone 是资产详情 device_protect_status 的固定返回值:设备保护期已下线,字段仅为兼容老版本客户端保留。 +const DeviceProtectStatusNone = "none" + +// 设备相关时长常量 const ( - DeviceProtectPeriodDuration = 1 * time.Hour // 设备停/复机保护期时长(1小时) DeviceRefreshCooldownDuration = 30 * time.Second // 设备网关刷新冷却时长(30秒) ) diff --git a/pkg/constants/iot.go b/pkg/constants/iot.go index 37d14f8e..6b31a814 100644 --- a/pkg/constants/iot.go +++ b/pkg/constants/iot.go @@ -72,7 +72,7 @@ const ( StopReasonTrafficExhausted = "traffic_exhausted" // 流量耗尽 StopReasonManual = "manual" // 手动停机 StopReasonArrears = "arrears" // 欠费 - StopReasonProtectPeriod = "protect_period" // 保护期一致性检查自动停机 + StopReasonProtectPeriod = "protect_period" // 保护期一致性检查自动停机(保护期已下线,不再产生新值;保留供历史数据与开放接口映射) StopReasonNoPackage = "no_package" // 停机原因:无有效套餐 StopReasonNotRealname = "not_realname" // 停机原因:非行业卡且未完成实名认证 StopReasonCarrierStopped = "carrier_stopped" // 停机原因:运营商/Gateway 侧主动停机 diff --git a/pkg/constants/polling.go b/pkg/constants/polling.go index 97ea5f74..6373be95 100644 --- a/pkg/constants/polling.go +++ b/pkg/constants/polling.go @@ -146,7 +146,7 @@ const ( ) // PollingPriorityTaskTypes 返回纳入优先轮询的既有轮询任务类型。 -// 一次优先需求对应这些任务类型各执行一次;保护期一致性检查不在集合内。 +// 一次优先需求对应这些任务类型各执行一次。 // 返回新切片,调用方修改不会影响其它调用方。 func PollingPriorityTaskTypes() []string { return []string{ diff --git a/pkg/constants/redis.go b/pkg/constants/redis.go index 72ce7b6f..e85dddac 100644 --- a/pkg/constants/redis.go +++ b/pkg/constants/redis.go @@ -417,16 +417,9 @@ func RedisClientPurchaseLockKey(assetType string, assetID uint) string { } // ======================================== -// 设备保护期相关 Redis Key +// 设备刷新相关 Redis Key // ======================================== -// RedisDeviceProtectKey 生成设备保护期 Redis 键 -// action: "stop"(停机保护期)或 "start"(复机保护期),两者互斥 -// 过期时间:DeviceProtectPeriodDuration(1 小时) -func RedisDeviceProtectKey(deviceID uint, action string) string { - return fmt.Sprintf("protect:device:%d:%s", deviceID, action) -} - // RedisDeviceRefreshCooldownKey 生成设备刷新冷却期 Redis 键 // 用途:防止同一设备短时间内多次调网关刷新(限频) // 过期时间:DeviceRefreshCooldownDuration(30 秒) @@ -434,16 +427,6 @@ func RedisDeviceRefreshCooldownKey(deviceID uint) string { return fmt.Sprintf("refresh:cooldown:device:%d", deviceID) } -// ======================================== -// 轮询保护期队列 Redis Key -// ======================================== - -// RedisPollingQueueProtectKey 保护期一致性检查轮询队列键 -// 用途:存储需要进行保护期一致性检查的卡 ID(ZSet,按时间排序) -func RedisPollingQueueProtectKey() string { - return "polling:queue:protect" -} - // ======================================== // 微信配置相关 Redis Key // ======================================== diff --git a/pkg/queue/handler.go b/pkg/queue/handler.go index 1d6b7f5d..dc7409f6 100644 --- a/pkg/queue/handler.go +++ b/pkg/queue/handler.go @@ -1,6 +1,8 @@ package queue import ( + "context" + "github.com/hibiken/asynq" "github.com/redis/go-redis/v9" "go.uber.org/zap" @@ -287,19 +289,16 @@ func (h *Handler) registerPollingHandlers() { packageHandler := task.NewPollingPackageHandler( h.pollingBase, h.workerResult.Stores.IotCard, h.pollingStopResumeSvc) - protectHandler := task.NewPollingProtectHandler( - h.db, h.workerResult.Services.ObservationSeriesEvents, - h.pollingBase, h.gatewayClient, h.workerResult.Stores.IotCard, - h.workerResult.Stores.DeviceSimBinding, h.pollingStopResumeSvc) cardStatusHandler := task.NewPollingCardStatusHandler( h.pollingBase, h.gatewayClient, h.workerResult.Services.CardObservation, integrationRepository) h.mux.HandleFunc(constants.TaskTypePollingRealname, realnameHandler.Handle) h.mux.HandleFunc(constants.TaskTypePollingCarddata, carddataHandler.Handle) h.mux.HandleFunc(constants.TaskTypePollingPackage, packageHandler.Handle) - h.mux.HandleFunc(constants.TaskTypePollingProtect, protectHandler.Handle) + // 设备保护期已下线:保留空处理函数消化发布前已入队的 polling:protect 任务,下一次发布再移除。 + h.mux.HandleFunc(constants.TaskTypePollingProtect, func(context.Context, *asynq.Task) error { return nil }) h.mux.HandleFunc(constants.TaskTypePollingCardStatus, cardStatusHandler.Handle) - h.logger.Info("已注册轮询任务处理器(realname/carddata/package/protect/card_status)") + h.logger.Info("已注册轮询任务处理器(realname/carddata/package/card_status)") } func (h *Handler) registerPackageActivationHandlers() {