feat(worker): add device registration_id cleanup worker
All checks were successful
Build Backend / build (push) Successful in 2m18s
Build Backend / build-docker (push) Successful in 1m19s

Add DeviceCleanupWorker to periodically clean up stale device tokens that haven't
been used within the retention period. This prevents accumulation of invalid
registration_ids and maintains data hygiene.

- New DeviceCleanupConfig with enabled, retention_days (3), and interval_minutes (360)
- New repository methods DeleteStaleDevices and DeleteOldestActiveDevice
- Updated RegisterDevice to evict oldest active device when reaching limit
- Reduced MaxDevicesPerUser from 10 to 3 per user
- Worker integrated into app startup/shutdown lifecycle
This commit is contained in:
lafay
2026-06-18 20:34:22 +08:00
parent cb85c62093
commit 322aa9eebb
10 changed files with 303 additions and 8 deletions

View File

@@ -28,7 +28,7 @@ const (
// PushQueueSize 推送队列大小
PushQueueSize = 1000
// MaxDevicesPerUser 每个用户最大设备数
MaxDevicesPerUser = 10
MaxDevicesPerUser = 3
)
// ChatMessageSender 聊天消息发送者信息
@@ -386,12 +386,9 @@ func (s *pushServiceImpl) pushViaJPushBatch(devices []*model.DeviceToken, params
// 处理同设备多账号场景registration_idpushToken是设备级标识
// 同一物理设备登录新账号时 pushToken 不变,需先失效其他用户对该 pushToken 的关联,
// 避免给一个账号推送时同设备的其他账号也收到通知。
// 每个用户最多保留 MaxDevicesPerUser 个活跃设备:注册后统一收敛到限额以内,
// 既能处理达上限淘汰,也能收敛历史超额数据(如限额从 10 调整为 3 后的存量清理)。
func (s *pushServiceImpl) RegisterDevice(ctx context.Context, userID string, deviceID string, deviceType model.DeviceType, pushToken string) error {
existing, err := s.deviceRepo.GetByUserID(userID)
if err == nil && len(existing) >= MaxDevicesPerUser {
return fmt.Errorf("exceeded maximum device limit (%d)", MaxDevicesPerUser)
}
// 失效其他用户/设备对同一 pushToken 的关联(防止跨账号误推)
if pushToken != "" {
if dErr := s.deviceRepo.DeactivateByPushToken(pushToken); dErr != nil {
@@ -413,7 +410,28 @@ func (s *pushServiceImpl) RegisterDevice(ctx context.Context, userID string, dev
}
deviceToken.UpdateLastUsed()
return s.deviceRepo.Upsert(deviceToken)
if err := s.deviceRepo.Upsert(deviceToken); err != nil {
return fmt.Errorf("failed to upsert device: %w", err)
}
// 收敛活跃设备数到 MaxDevicesPerUser 以内(保留最近使用的,淘汰超额的最旧设备)
// 覆盖两种场景:① 新设备注册导致超限;② 历史存量超额(限额调整后逐步收敛)
if evicted, err := s.deviceRepo.TrimActiveDevices(userID, MaxDevicesPerUser); err != nil {
zap.L().Warn("trim active devices failed",
zap.String("userID", userID),
zap.Int("limit", MaxDevicesPerUser),
zap.Error(err),
)
} else if evicted > 0 {
zap.L().Info("evicted oldest devices for device limit",
zap.String("userID", userID),
zap.String("deviceID", deviceID),
zap.Int64("evicted", evicted),
zap.Int("limit", MaxDevicesPerUser),
)
}
return nil
}
// UnregisterDevice 注销设备