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

@@ -31,6 +31,12 @@ type DeviceTokenRepository interface {
GetDeviceCountByUserID(userID int64) (int64, error)
GetActiveDeviceCountByUserID(userID int64) (int64, error)
DeleteInactiveDevices(before time.Time) error
// DeleteStaleDevices 删除超过指定时间未使用的设备(按 last_used_at 判断,不限 is_active
// 用于周期性清理长期不活跃的 registration_idlast_used_at 为 NULL 的设备不清理
DeleteStaleDevices(before time.Time) error
// TrimActiveDevices 将用户活跃设备数量收敛到 keep 以内,删除超额的最旧设备
// 返回实际删除的记录数。用于设备数限制(如限制从 10 改为 3 后的存量收敛)
TrimActiveDevices(userID string, keep int) (int64, error)
}
// deviceTokenRepository 设备Token仓储实现
@@ -200,3 +206,31 @@ func (r *deviceTokenRepository) DeleteInactiveDevices(before time.Time) error {
return r.db.Where("is_active = ? AND last_used_at < ?", false, before).
Delete(&model.DeviceToken{}).Error
}
// DeleteStaleDevices 删除超过指定时间未使用的设备(按 last_used_at 判断,不限 is_active
// last_used_at 为 NULL 的设备不清理(可能是刚注册尚未使用)
func (r *deviceTokenRepository) DeleteStaleDevices(before time.Time) error {
return r.db.Where("last_used_at IS NOT NULL AND last_used_at < ?", before).
Delete(&model.DeviceToken{}).Error
}
// TrimActiveDevices 将用户活跃设备数量收敛到 keep 以内,删除超额的最旧设备
// 按 last_used_at 降序保留最新的 keep 条活跃记录,删除其余活跃记录。
// keep <= 0 时不删除任何记录。返回实际删除的记录数。
// 用于设备数限制(新注册超限淘汰 + 历史存量超额收敛)。
func (r *deviceTokenRepository) TrimActiveDevices(userID string, keep int) (int64, error) {
if keep <= 0 {
return 0, nil
}
// 子查询:取该用户最新的 keep 条活跃记录 ID
subQuery := r.db.Model(&model.DeviceToken{}).
Select("id").
Where("user_id = ? AND is_active = ?", userID, true).
Order("last_used_at DESC NULLS LAST").
Limit(keep)
// 删除不在保留集合内的活跃记录
result := r.db.Where("user_id = ? AND is_active = ? AND id NOT IN (?)", userID, true, subQuery).
Delete(&model.DeviceToken{})
return result.RowsAffected, result.Error
}