feat(privacy): add user privacy settings and account deletion system
Add privacy settings allowing users to control visibility of their followers, following, posts, and favorites lists with three visibility levels: everyone, following, and self. Also implement account deletion workflow with 30-day cooldown period. - Add PrivacySettings model with visibility levels - Add DTOs for privacy settings and account deletion - Add repository methods for privacy settings and deletion - Add service methods to check visibility permissions - Add handlers for get/update privacy settings and account deletion - Add new routes for privacy settings and account management - Add account cleanup service for hard-deleting expired accounts - Modify login to cancel deletion when user logs in BREAKING CHANGE: Users with pending_deletion status can no longer log in; logging in now automatically cancels deletion requests.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/cache"
|
||||
apperrors "carrot_bbs/internal/errors"
|
||||
@@ -52,6 +53,20 @@ type UserService interface {
|
||||
UnblockUser(ctx context.Context, blockerID, blockedID string) error
|
||||
GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error)
|
||||
IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error)
|
||||
|
||||
// 隐私设置
|
||||
GetPrivacySettings(ctx context.Context, userID string) (*model.PrivacySettings, error)
|
||||
UpdatePrivacySettings(ctx context.Context, userID string, settings *model.PrivacySettings) error
|
||||
CheckVisibility(ctx context.Context, viewerID, targetUserID string, visibility model.VisibilityLevel) (bool, error)
|
||||
CanViewFollowers(ctx context.Context, viewerID, targetUserID string) (bool, error)
|
||||
CanViewFollowing(ctx context.Context, viewerID, targetUserID string) (bool, error)
|
||||
CanViewPosts(ctx context.Context, viewerID, targetUserID string) (bool, error)
|
||||
CanViewFavorites(ctx context.Context, viewerID, targetUserID string) (bool, error)
|
||||
|
||||
// 账号注销
|
||||
RequestAccountDeletion(ctx context.Context, userID, password string) error
|
||||
CancelAccountDeletion(ctx context.Context, userID string) error
|
||||
GetDeletionStatus(ctx context.Context, userID string) (isPending bool, requestedAt *time.Time, daysLeft int, err error)
|
||||
}
|
||||
|
||||
// userServiceImpl 用户服务实现
|
||||
@@ -320,11 +335,10 @@ func (s *userServiceImpl) Login(ctx context.Context, account, password string) (
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
if user.Status != model.UserStatusActive {
|
||||
if user.Status != model.UserStatusActive && user.Status != model.UserStatusPendingDeletion {
|
||||
loginResult = string(model.LoginResultFail)
|
||||
failReason = string(model.FailReasonAccountBanned)
|
||||
|
||||
// 记录失败登录日志
|
||||
if s.logService != nil {
|
||||
s.logService.LoginLog.RecordLogin(&model.LoginLog{
|
||||
UserID: user.ID,
|
||||
@@ -341,6 +355,13 @@ func (s *userServiceImpl) Login(ctx context.Context, account, password string) (
|
||||
return nil, ErrUserBanned
|
||||
}
|
||||
|
||||
// 如果用户已申请注销,登录即取消注销
|
||||
if user.DeletionRequestedAt != nil {
|
||||
_ = s.userRepo.CancelDeletion(user.ID)
|
||||
user.DeletionRequestedAt = nil
|
||||
user.Status = model.UserStatusActive
|
||||
}
|
||||
|
||||
// 记录成功登录日志
|
||||
if s.logService != nil {
|
||||
s.logService.LoginLog.RecordLogin(&model.LoginLog{
|
||||
@@ -700,6 +721,107 @@ func (s *userServiceImpl) Search(ctx context.Context, keyword string, page, page
|
||||
return s.userRepo.Search(keyword, page, pageSize)
|
||||
}
|
||||
|
||||
// GetPrivacySettings 获取隐私设置
|
||||
func (s *userServiceImpl) GetPrivacySettings(ctx context.Context, userID string) (*model.PrivacySettings, error) {
|
||||
return s.userRepo.GetPrivacySettings(userID)
|
||||
}
|
||||
|
||||
// UpdatePrivacySettings 更新隐私设置
|
||||
func (s *userServiceImpl) UpdatePrivacySettings(ctx context.Context, userID string, settings *model.PrivacySettings) error {
|
||||
return s.userRepo.UpdatePrivacySettings(userID, settings)
|
||||
}
|
||||
|
||||
// CheckVisibility 检查可见性权限
|
||||
func (s *userServiceImpl) CheckVisibility(ctx context.Context, viewerID, targetUserID string, visibility model.VisibilityLevel) (bool, error) {
|
||||
switch visibility {
|
||||
case model.VisibilityEveryone:
|
||||
return true, nil
|
||||
case model.VisibilityFollowing:
|
||||
if viewerID == "" {
|
||||
return false, nil
|
||||
}
|
||||
return s.userRepo.IsFollowing(viewerID, targetUserID)
|
||||
case model.VisibilitySelf:
|
||||
return viewerID == targetUserID, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// CanViewFollowers 检查是否可以查看粉丝列表
|
||||
func (s *userServiceImpl) CanViewFollowers(ctx context.Context, viewerID, targetUserID string) (bool, error) {
|
||||
settings, err := s.userRepo.GetPrivacySettings(targetUserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s.CheckVisibility(ctx, viewerID, targetUserID, settings.FollowersVisibility)
|
||||
}
|
||||
|
||||
// CanViewFollowing 检查是否可以查看关注列表
|
||||
func (s *userServiceImpl) CanViewFollowing(ctx context.Context, viewerID, targetUserID string) (bool, error) {
|
||||
settings, err := s.userRepo.GetPrivacySettings(targetUserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s.CheckVisibility(ctx, viewerID, targetUserID, settings.FollowingVisibility)
|
||||
}
|
||||
|
||||
// CanViewPosts 检查是否可以查看帖子列表
|
||||
func (s *userServiceImpl) CanViewPosts(ctx context.Context, viewerID, targetUserID string) (bool, error) {
|
||||
settings, err := s.userRepo.GetPrivacySettings(targetUserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s.CheckVisibility(ctx, viewerID, targetUserID, settings.PostsVisibility)
|
||||
}
|
||||
|
||||
// CanViewFavorites 检查是否可以查看收藏列表
|
||||
func (s *userServiceImpl) CanViewFavorites(ctx context.Context, viewerID, targetUserID string) (bool, error) {
|
||||
settings, err := s.userRepo.GetPrivacySettings(targetUserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s.CheckVisibility(ctx, viewerID, targetUserID, settings.FavoritesVisibility)
|
||||
}
|
||||
|
||||
// RequestAccountDeletion 申请注销账号
|
||||
func (s *userServiceImpl) RequestAccountDeletion(ctx context.Context, userID, password string) error {
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
if !utils.CheckPasswordHash(password, user.PasswordHash) {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
|
||||
return s.userRepo.RequestDeletion(userID)
|
||||
}
|
||||
|
||||
// CancelAccountDeletion 取消注销
|
||||
func (s *userServiceImpl) CancelAccountDeletion(ctx context.Context, userID string) error {
|
||||
return s.userRepo.CancelDeletion(userID)
|
||||
}
|
||||
|
||||
// GetDeletionStatus 获取注销状态
|
||||
func (s *userServiceImpl) GetDeletionStatus(ctx context.Context, userID string) (isPending bool, requestedAt *time.Time, daysLeft int, err error) {
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
return false, nil, 0, err
|
||||
}
|
||||
|
||||
if user.DeletionRequestedAt == nil {
|
||||
return false, nil, 0, nil
|
||||
}
|
||||
|
||||
daysLeft = 90 - int(time.Since(*user.DeletionRequestedAt).Hours()/24)
|
||||
if daysLeft < 0 {
|
||||
daysLeft = 0
|
||||
}
|
||||
|
||||
return true, user.DeletionRequestedAt, daysLeft, nil
|
||||
}
|
||||
|
||||
// 错误定义 - 使用统一的 AppError
|
||||
var (
|
||||
ErrInvalidUsername = apperrors.ErrInvalidUsername
|
||||
|
||||
Reference in New Issue
Block a user