Files
backend/internal/service/account_cleanup_service.go
lan c630cbf4d0
All checks were successful
Build Backend / build (push) Successful in 2m0s
Build Backend / build-docker (push) Successful in 1m26s
feat(ws): implement WebSocket cluster mode with Redis Pub/Sub
Introduce a new WebSocket messaging architecture that supports both standalone and cluster modes. This allows for horizontal scaling of WebSocket servers by using Redis Pub/Sub to synchronize messages across multiple instances.

Key changes:
- Added `ws.MessagePublisher` interface to abstract message distribution.
- Implemented `ws.Bus` to handle cluster-mode messaging via Redis.
- Added `ws.OnlineTracker` to manage user online status across the cluster.
- Refactored multiple services (Chat, Group, Push, Call, etc.) to use the new `MessagePublisher` instead of a concrete `ws.Hub`.
- Added WebSocket configuration options (mode, instance ID, channel, TTL, heartbeat) to `config.yaml` and `config.go`.
- Updated dependency injection with Wire to support the new publisher and Redis client.
- Improved logging by replacing standard `log` with `zap` in several service components.
2026-05-06 12:39:11 +08:00

115 lines
2.6 KiB
Go

package service
import (
"context"
"time"
"with_you/internal/model"
"with_you/internal/repository"
"go.uber.org/zap"
"gorm.io/gorm"
)
const AccountDeletionCoolDownDays = 90
type AccountCleanupService interface {
CleanupExpiredAccounts(ctx context.Context) (int64, error)
}
type accountCleanupServiceImpl struct {
db *gorm.DB
userRepo repository.UserRepository
logService *LogService
}
func NewAccountCleanupService(
db *gorm.DB,
userRepo repository.UserRepository,
logService *LogService,
) AccountCleanupService {
return &accountCleanupServiceImpl{
db: db,
userRepo: userRepo,
logService: logService,
}
}
func (s *accountCleanupServiceImpl) CleanupExpiredAccounts(ctx context.Context) (int64, error) {
cutoffTime := time.Now().AddDate(0, 0, -AccountDeletionCoolDownDays)
users, err := s.userRepo.GetPendingDeletionUsers(cutoffTime)
if err != nil {
return 0, err
}
if len(users) == 0 {
return 0, nil
}
var deletedCount int64
for _, user := range users {
if err := s.anonymizeAndDeleteUser(ctx, user); err != nil {
zap.L().Error("Failed to cleanup user",
zap.String("user_id", user.ID),
zap.Error(err),
)
continue
}
deletedCount++
}
zap.L().Info("Cleaned up expired accounts",
zap.Int64("count", deletedCount),
)
return deletedCount, nil
}
func (s *accountCleanupServiceImpl) anonymizeAndDeleteUser(ctx context.Context, user *model.User) error {
deletedUserID := "deleted_user"
return s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&model.Post{}).Where("user_id = ?", user.ID).Update("user_id", deletedUserID).Error; err != nil {
return err
}
if err := tx.Model(&model.Comment{}).Where("user_id = ?", user.ID).Update("user_id", deletedUserID).Error; err != nil {
return err
}
if err := tx.Where("follower_id = ? OR following_id = ?", user.ID, user.ID).Delete(&model.Follow{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", user.ID).Delete(&model.Favorite{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", user.ID).Delete(&model.PostLike{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", user.ID).Delete(&model.CommentLike{}).Error; err != nil {
return err
}
if err := tx.Unscoped().Delete(&model.User{}, "id = ?", user.ID).Error; err != nil {
return err
}
if s.logService != nil {
s.logService.OperationLog.RecordOperation(&model.OperationLog{
UserID: user.ID,
UserName: user.Username,
Operation: "account_deleted",
Module: "user",
TargetType: "user",
TargetID: user.ID,
Status: "success",
})
}
return nil
})
}