Files
backend/internal/repository/device_token_repo.go
lafay 322aa9eebb
All checks were successful
Build Backend / build (push) Successful in 2m18s
Build Backend / build-docker (push) Successful in 1m19s
feat(worker): add device registration_id cleanup worker
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
2026-06-18 20:36:25 +08:00

237 lines
8.5 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 repository
import (
"time"
"with_you/internal/model"
"gorm.io/gorm"
)
// DeviceTokenRepository 设备Token仓储接口
type DeviceTokenRepository interface {
Create(token *model.DeviceToken) error
GetByID(id int64) (*model.DeviceToken, error)
Update(token *model.DeviceToken) error
Delete(id int64) error
GetByUserID(userID string) ([]*model.DeviceToken, error)
GetActiveByUserID(userID string) ([]*model.DeviceToken, error)
GetByDeviceID(deviceID string) (*model.DeviceToken, error)
GetByPushToken(pushToken string) (*model.DeviceToken, error)
// DeactivateByPushToken 失效指定 pushToken 的所有记录
// 用于处理同设备多账号场景registration_id 是设备级标识,
// 当新账号在同一设备注册时,需失效旧账号对该 pushToken 的关联,避免跨账号误推
DeactivateByPushToken(pushToken string) error
DeactivateAllExcept(userID int64, deviceID string) error
Upsert(token *model.DeviceToken) error
UpdateLastUsed(deviceID string) error
Deactivate(deviceID string) error
Activate(deviceID string) error
DeleteByUserID(userID int64) error
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仓储实现
type deviceTokenRepository struct {
db *gorm.DB
}
// NewDeviceTokenRepository 创建设备Token仓储
func NewDeviceTokenRepository(db *gorm.DB) DeviceTokenRepository {
return &deviceTokenRepository{db: db}
}
// Create 创建设备Token
func (r *deviceTokenRepository) Create(token *model.DeviceToken) error {
return r.db.Create(token).Error
}
// GetByID 根据ID获取设备Token
func (r *deviceTokenRepository) GetByID(id int64) (*model.DeviceToken, error) {
var token model.DeviceToken
err := r.db.First(&token, "id = ?", id).Error
if err != nil {
return nil, err
}
return &token, nil
}
// Update 更新设备Token
func (r *deviceTokenRepository) Update(token *model.DeviceToken) error {
return r.db.Save(token).Error
}
// Delete 删除设备Token软删除
func (r *deviceTokenRepository) Delete(id int64) error {
return r.db.Delete(&model.DeviceToken{}, id).Error
}
// GetByUserID 获取用户所有设备
// userID 参数为 string 类型UUID格式与JWT中user_id保持一致
func (r *deviceTokenRepository) GetByUserID(userID string) ([]*model.DeviceToken, error) {
var tokens []*model.DeviceToken
err := r.db.Where("user_id = ?", userID).
Order("created_at DESC").
Find(&tokens).Error
return tokens, err
}
// GetActiveByUserID 获取用户活跃设备
// userID 参数为 string 类型UUID格式与JWT中user_id保持一致
func (r *deviceTokenRepository) GetActiveByUserID(userID string) ([]*model.DeviceToken, error) {
var tokens []*model.DeviceToken
err := r.db.Where("user_id = ? AND is_active = ?", userID, true).
Order("last_used_at DESC").
Find(&tokens).Error
return tokens, err
}
// GetByDeviceID 根据设备ID获取设备Token
func (r *deviceTokenRepository) GetByDeviceID(deviceID string) (*model.DeviceToken, error) {
var token model.DeviceToken
err := r.db.Where("device_id = ?", deviceID).First(&token).Error
if err != nil {
return nil, err
}
return &token, nil
}
// GetByPushToken 根据推送Token获取设备信息
func (r *deviceTokenRepository) GetByPushToken(pushToken string) (*model.DeviceToken, error) {
var token model.DeviceToken
err := r.db.Where("push_token = ?", pushToken).First(&token).Error
if err != nil {
return nil, err
}
return &token, nil
}
// DeactivateByPushToken 失效指定 pushToken 的所有记录
// registration_idpushToken是设备级标识同一物理设备登录不同账号时 pushToken 不变。
// 失效旧关联可防止给账号 A 推送时,同设备的账号 B 也收到通知。
func (r *deviceTokenRepository) DeactivateByPushToken(pushToken string) error {
return r.db.Model(&model.DeviceToken{}).
Where("push_token = ?", pushToken).
Update("is_active", false).Error
}
// DeactivateAllExcept 登出其他设备(停用除指定设备外的所有设备)
func (r *deviceTokenRepository) DeactivateAllExcept(userID int64, deviceID string) error {
return r.db.Model(&model.DeviceToken{}).
Where("user_id = ? AND device_id != ?", userID, deviceID).
Update("is_active", false).Error
}
// Upsert 创建或更新设备Token
// 如果设备ID已存在则更新Token和激活状态否则创建新记录
func (r *deviceTokenRepository) Upsert(token *model.DeviceToken) error {
var existing model.DeviceToken
err := r.db.Where("device_id = ?", token.DeviceID).First(&existing).Error
if err == gorm.ErrRecordNotFound {
// 创建新记录
return r.db.Create(token).Error
} else if err != nil {
return err
}
// 更新现有记录(包括 user_id设备可能在用户间转移
return r.db.Model(&existing).Updates(map[string]any{
"user_id": token.UserID,
"push_token": token.PushToken,
"device_type": token.DeviceType,
"is_active": true,
"device_name": token.DeviceName,
"last_used_at": time.Now(),
}).Error
}
// UpdateLastUsed 更新最后使用时间
func (r *deviceTokenRepository) UpdateLastUsed(deviceID string) error {
return r.db.Model(&model.DeviceToken{}).
Where("device_id = ?", deviceID).
Update("last_used_at", time.Now()).Error
}
// Deactivate 停用设备
func (r *deviceTokenRepository) Deactivate(deviceID string) error {
return r.db.Model(&model.DeviceToken{}).
Where("device_id = ?", deviceID).
Update("is_active", false).Error
}
// Activate 激活设备
func (r *deviceTokenRepository) Activate(deviceID string) error {
return r.db.Model(&model.DeviceToken{}).
Where("device_id = ?", deviceID).
Updates(map[string]any{
"is_active": true,
"last_used_at": time.Now(),
}).Error
}
// DeleteByUserID 删除用户所有设备Token
func (r *deviceTokenRepository) DeleteByUserID(userID int64) error {
return r.db.Where("user_id = ?", userID).Delete(&model.DeviceToken{}).Error
}
// GetDeviceCountByUserID 获取用户设备数量
func (r *deviceTokenRepository) GetDeviceCountByUserID(userID int64) (int64, error) {
var count int64
err := r.db.Model(&model.DeviceToken{}).
Where("user_id = ?", userID).
Count(&count).Error
return count, err
}
// GetActiveDeviceCountByUserID 获取用户活跃设备数量
func (r *deviceTokenRepository) GetActiveDeviceCountByUserID(userID int64) (int64, error) {
var count int64
err := r.db.Model(&model.DeviceToken{}).
Where("user_id = ? AND is_active = ?", userID, true).
Count(&count).Error
return count, err
}
// DeleteInactiveDevices 删除长时间未使用的设备
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
}