Files
junhong_cmp_fiber/internal/polling/lifecycle_service.go
break 8cf921f11c
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
驳回接口
2026-06-25 17:10:24 +09:00

186 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package polling
import (
"context"
"math/rand"
"time"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
)
// PollingLifecycleService 卡生命周期轮询管理服务
// 替代 callbacks.go 和 api_callback.go 中的生命周期方法
// 实现 iot_card.PollingCallback 接口两个进程API 和 Worker共享同一实现
// 依赖PollingQueueManager队列操作+ PollingConfigManager配置匹配
type PollingLifecycleService struct {
queueMgr *PollingQueueManager
configMgr *PollingConfigManager
iotCardStore *postgres.IotCardStore
deviceBindingStore *postgres.DeviceSimBindingStore
deviceStore *postgres.DeviceStore
logger *zap.Logger
}
// NewPollingLifecycleService 创建卡生命周期轮询管理服务
func NewPollingLifecycleService(
queueMgr *PollingQueueManager,
configMgr *PollingConfigManager,
iotCardStore *postgres.IotCardStore,
deviceBindingStore *postgres.DeviceSimBindingStore,
deviceStore *postgres.DeviceStore,
logger *zap.Logger,
) *PollingLifecycleService {
return &PollingLifecycleService{
queueMgr: queueMgr,
configMgr: configMgr,
iotCardStore: iotCardStore,
deviceBindingStore: deviceBindingStore,
deviceStore: deviceStore,
logger: logger,
}
}
// OnCardCreated 新卡创建后初始化轮询
func (s *PollingLifecycleService) OnCardCreated(ctx context.Context, card *model.IotCard) {
if card == nil {
return
}
if !s.shouldEnqueue(ctx, card) {
s.logger.Debug("卡禁用轮询,跳过入队", zap.Uint("card_id", card.ID))
return
}
s.enqueueCard(ctx, card)
}
// OnBatchCardsCreated 批量卡创建后批量初始化轮询
func (s *PollingLifecycleService) OnBatchCardsCreated(ctx context.Context, cards []*model.IotCard) {
for _, card := range cards {
s.OnCardCreated(ctx, card)
}
}
// OnCardStatusChanged 卡状态变化后清理缓存、重新匹配配置并更新队列
func (s *PollingLifecycleService) OnCardStatusChanged(ctx context.Context, cardID uint) {
if err := s.queueMgr.RemoveFromAllQueues(ctx, cardID); err != nil {
s.logger.Warn("卡状态变化:从队列移除失败", zap.Uint("card_id", cardID), zap.Error(err))
}
// 清理轮询卡缓存,确保下次轮询从 DB 读取最新状态
s.queueMgr.InvalidateCardCache(ctx, cardID)
card, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
s.logger.Error("卡状态变化:加载卡信息失败", zap.Uint("card_id", cardID), zap.Error(err))
return
}
if !s.shouldEnqueue(ctx, card) {
return
}
s.enqueueCard(ctx, card)
}
// OnBatchCardsStatusChanged 批量卡状态变化:单次 Redis pipeline 清理 + 单次 DB 批量查询
// 替代 N 次 OnCardStatusChanged 调用,避免批量分配/回收时打满 DB 连接池
func (s *PollingLifecycleService) OnBatchCardsStatusChanged(ctx context.Context, cardIDs []uint) {
if len(cardIDs) == 0 {
return
}
// 单次 pipeline批量从当前分片队列移除并清理缓存
if err := s.queueMgr.RemoveBatchFromCurrentShardQueues(ctx, cardIDs); err != nil {
s.logger.Warn("批量卡状态变化:从队列移除失败", zap.Int("count", len(cardIDs)), zap.Error(err))
}
// 单次 DB 查询:批量加载所有卡信息
cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs)
if err != nil {
s.logger.Error("批量卡状态变化:加载卡信息失败", zap.Int("count", len(cardIDs)), zap.Error(err))
return
}
for _, card := range cards {
if !s.shouldEnqueue(ctx, card) {
continue
}
s.enqueueCard(ctx, card)
}
}
// OnCardDeleted 卡删除后移除所有队列并清理缓存
func (s *PollingLifecycleService) OnCardDeleted(ctx context.Context, cardID uint) {
if err := s.queueMgr.OnCardDeleted(ctx, cardID); err != nil {
s.logger.Warn("卡删除:清理队列和缓存失败", zap.Uint("card_id", cardID), zap.Error(err))
}
}
// OnCardEnabled 卡启用轮询后初始化
func (s *PollingLifecycleService) OnCardEnabled(ctx context.Context, cardID uint) {
s.queueMgr.InvalidateCardCache(ctx, cardID)
card, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
s.logger.Error("卡启用:加载卡信息失败", zap.Uint("card_id", cardID), zap.Error(err))
return
}
if !s.shouldEnqueue(ctx, card) {
return
}
s.enqueueCard(ctx, card)
}
// OnCardDisabled 卡禁用轮询后移除所有队列
func (s *PollingLifecycleService) OnCardDisabled(ctx context.Context, cardID uint) {
if err := s.queueMgr.RemoveFromAllQueues(ctx, cardID); err != nil {
s.logger.Warn("卡禁用:从队列移除失败", zap.Uint("card_id", cardID), zap.Error(err))
}
s.queueMgr.InvalidateCardCache(ctx, cardID)
}
// shouldEnqueue M3 修复:检查卡或其绑定设备是否允许轮询
// 两层检查:先查卡级 enable_polling再查设备级 enable_pollingIsStandalone=false 时)
// 直接查 device.enable_polling不依赖级联同步正确性防止同步失败时生命周期事件绕过设备级禁用
func (s *PollingLifecycleService) shouldEnqueue(ctx context.Context, card *model.IotCard) bool {
if !card.EnablePolling {
return false
}
if !card.IsStandalone && s.deviceBindingStore != nil && s.deviceStore != nil {
binding, err := s.deviceBindingStore.GetActiveBindingByCardID(ctx, card.ID)
if err == nil && binding != nil {
device, devErr := s.deviceStore.GetByID(ctx, binding.DeviceID)
if devErr == nil && !device.EnablePolling {
s.logger.Debug("设备已禁用轮询,跳过卡入队",
zap.Uint("card_id", card.ID), zap.Uint("device_id", binding.DeviceID))
return false
}
}
}
return true
}
// enqueueCard 按匹配配置将卡入队分片队列
func (s *PollingLifecycleService) enqueueCard(ctx context.Context, card *model.IotCard) {
intervals := s.configMgr.MergedTaskIntervals(card)
if len(intervals) == 0 {
return
}
for taskType, info := range intervals {
if err := s.queueMgr.Requeue(ctx, card.ID, taskType, calcInitialDelay(info.Interval)); err != nil {
s.logger.Warn("卡入队失败",
zap.Uint("card_id", card.ID), zap.String("task_type", taskType), zap.Error(err))
}
}
}
// calcInitialDelay 计算新入队卡的初始检查时间(加随机抖动避免惊群)
// 抖动范围 [0, max(interval/10, 2)) 秒;下界取 2 保证 rand.Intn 有实际随机性
// rand.Intn(1) 永远返回 0对 interval≤10 的配置会导致零抖动)
func calcInitialDelay(interval int) time.Time {
now := time.Now()
if interval <= 0 {
return now
}
jitterMax := interval / 10
if jitterMax < 2 {
jitterMax = 2
}
return now.Add(time.Duration(rand.Intn(jitterMax)) * time.Second)
}