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
237 lines
8.5 KiB
Go
237 lines
8.5 KiB
Go
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_id,last_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_id(pushToken)是设备级标识,同一物理设备登录不同账号时 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
|
||
}
|