feat(privacy): add user privacy settings and account deletion system
All checks were successful
Build Backend / build (push) Successful in 5m16s
Build Backend / build-docker (push) Successful in 3m44s

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:
lafay
2026-04-08 14:55:50 +08:00
parent 9440df66ba
commit 539bec6f63
9 changed files with 642 additions and 76 deletions

View File

@@ -44,6 +44,8 @@ type UserSelfResponse struct {
FollowingCount int `json:"following_count"`
IsVerified bool `json:"is_verified"`
CreatedAt string `json:"created_at"`
// 隐私设置
PrivacySettings *PrivacySettingsResponse `json:"privacy_settings,omitempty"`
}
// UserDetailResponse 用户详情响应(仅本人可见,包含敏感信息)
@@ -66,6 +68,8 @@ type UserDetailResponse struct {
IsFollowing bool `json:"is_following"`
IsFollowingMe bool `json:"is_following_me"`
CreatedAt string `json:"created_at"`
// 隐私设置
PrivacySettings *PrivacySettingsResponse `json:"privacy_settings,omitempty"`
}
// ==================== Admin User DTOs ====================
@@ -1403,3 +1407,33 @@ type AdminReviewVerificationRequest struct {
Approve bool `json:"approve" binding:"required"`
RejectReason string `json:"reject_reason" binding:"omitempty,max=500"`
}
// ==================== Privacy & Account Deletion DTOs ====================
// PrivacySettingsRequest 隐私设置请求
type PrivacySettingsRequest struct {
FollowersVisibility *string `json:"followers_visibility" binding:"omitempty,oneof=everyone following self"`
FollowingVisibility *string `json:"following_visibility" binding:"omitempty,oneof=everyone following self"`
PostsVisibility *string `json:"posts_visibility" binding:"omitempty,oneof=everyone following self"`
FavoritesVisibility *string `json:"favorites_visibility" binding:"omitempty,oneof=everyone following self"`
}
// PrivacySettingsResponse 隐私设置响应
type PrivacySettingsResponse struct {
FollowersVisibility string `json:"followers_visibility"`
FollowingVisibility string `json:"following_visibility"`
PostsVisibility string `json:"posts_visibility"`
FavoritesVisibility string `json:"favorites_visibility"`
}
// RequestDeletionRequest 申请注销请求
type RequestDeletionRequest struct {
Password string `json:"password" binding:"required"`
}
// DeletionStatusResponse 注销状态响应
type DeletionStatusResponse struct {
IsPendingDeletion bool `json:"is_pending_deletion"`
DeletionRequestedAt string `json:"deletion_requested_at,omitempty"`
CoolDownDays int `json:"cool_down_days,omitempty"`
}

View File

@@ -56,6 +56,19 @@ func ConvertUserToSelfResponse(user *model.User, postsCount int) *UserSelfRespon
FollowingCount: user.FollowingCount,
IsVerified: user.IsVerified,
CreatedAt: FormatTime(user.CreatedAt),
PrivacySettings: convertPrivacySettingsToResponse(&user.PrivacySettings),
}
}
func convertPrivacySettingsToResponse(ps *model.PrivacySettings) *PrivacySettingsResponse {
if ps == nil {
return nil
}
return &PrivacySettingsResponse{
FollowersVisibility: string(ps.FollowersVisibility),
FollowingVisibility: string(ps.FollowingVisibility),
PostsVisibility: string(ps.PostsVisibility),
FavoritesVisibility: string(ps.FavoritesVisibility),
}
}
@@ -174,6 +187,7 @@ func ConvertUserToDetailResponse(user *model.User) *UserDetailResponse {
FollowingCount: user.FollowingCount,
IsVerified: user.IsVerified,
CreatedAt: FormatTime(user.CreatedAt),
PrivacySettings: convertPrivacySettingsToResponse(&user.PrivacySettings),
}
}
@@ -199,6 +213,7 @@ func ConvertUserToDetailResponseWithPostsCount(user *model.User, postsCount int)
FollowingCount: user.FollowingCount,
IsVerified: user.IsVerified,
CreatedAt: FormatTime(user.CreatedAt),
PrivacySettings: convertPrivacySettingsToResponse(&user.PrivacySettings),
}
}

View File

@@ -553,6 +553,19 @@ func (h *PostHandler) Unfavorite(c *gin.Context) {
// GetUserPosts 获取用户帖子列表(支持游标分页和偏移分页)
func (h *PostHandler) GetUserPosts(c *gin.Context) {
userID := c.Param("id")
currentUserID := c.GetString("user_id")
canView, err := h.userService.CanViewPosts(c.Request.Context(), currentUserID, userID)
if err != nil {
response.InternalServerError(c, "failed to check privacy settings")
return
}
if !canView {
response.Forbidden(c, "该用户的帖子列表不对外公开")
return
}
// 检查是否使用游标分页
cursorStr := c.Query("cursor")
if cursorStr != "" {
@@ -561,12 +574,10 @@ func (h *PostHandler) GetUserPosts(c *gin.Context) {
}
// 偏移分页(向后兼容)
userID := c.Param("id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
page, pageSize = normalizePagination(page, pageSize)
currentUserID := c.GetString("user_id")
includePending := currentUserID != "" && currentUserID == userID
posts, total, err := h.postService.GetUserPosts(c.Request.Context(), userID, page, pageSize, includePending)
if err != nil {
@@ -629,6 +640,18 @@ func (h *PostHandler) GetUserPostsByCursor(c *gin.Context) {
// GetFavorites 获取收藏列表
func (h *PostHandler) GetFavorites(c *gin.Context) {
userID := c.Param("id")
currentUserID := c.GetString("user_id")
canView, err := h.userService.CanViewFavorites(c.Request.Context(), currentUserID, userID)
if err != nil {
response.InternalServerError(c, "failed to check privacy settings")
return
}
if !canView {
response.Forbidden(c, "该用户的收藏列表不对外公开")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
page, pageSize = normalizePagination(page, pageSize)
@@ -639,8 +662,6 @@ func (h *PostHandler) GetFavorites(c *gin.Context) {
return
}
// 批量获取交互状态
currentUserID := c.GetString("user_id")
postIDs := make([]string, len(posts))
for i, post := range posts {
postIDs[i] = post.ID

View File

@@ -615,6 +615,17 @@ func (h *UserHandler) GetBlockStatus(c *gin.Context) {
func (h *UserHandler) GetFollowingList(c *gin.Context) {
userID := c.Param("id")
currentUserID := c.GetString("user_id")
canView, err := h.userService.CanViewFollowing(c.Request.Context(), currentUserID, userID)
if err != nil {
response.InternalServerError(c, "failed to check privacy settings")
return
}
if !canView {
response.Forbidden(c, "该用户的关注列表不对外公开")
return
}
page := c.DefaultQuery("page", "1")
pageSize := c.DefaultQuery("page_size", "20")
@@ -654,6 +665,17 @@ func (h *UserHandler) GetFollowingList(c *gin.Context) {
func (h *UserHandler) GetFollowersList(c *gin.Context) {
userID := c.Param("id")
currentUserID := c.GetString("user_id")
canView, err := h.userService.CanViewFollowers(c.Request.Context(), currentUserID, userID)
if err != nil {
response.InternalServerError(c, "failed to check privacy settings")
return
}
if !canView {
response.Forbidden(c, "该用户的粉丝列表不对外公开")
return
}
page := c.DefaultQuery("page", "1")
pageSize := c.DefaultQuery("page_size", "20")
@@ -810,3 +832,151 @@ func (h *UserHandler) Search(c *gin.Context) {
response.Paginated(c, userResponses, total, page, pageSize)
}
// GetPrivacySettings 获取隐私设置
func (h *UserHandler) GetPrivacySettings(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
settings, err := h.userService.GetPrivacySettings(c.Request.Context(), userID)
if err != nil {
response.InternalServerError(c, "failed to get privacy settings")
return
}
response.Success(c, &dto.PrivacySettingsResponse{
FollowersVisibility: string(settings.FollowersVisibility),
FollowingVisibility: string(settings.FollowingVisibility),
PostsVisibility: string(settings.PostsVisibility),
FavoritesVisibility: string(settings.FavoritesVisibility),
})
}
// UpdatePrivacySettings 更新隐私设置
func (h *UserHandler) UpdatePrivacySettings(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
var req dto.PrivacySettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
settings := &model.PrivacySettings{}
if req.FollowersVisibility != nil {
settings.FollowersVisibility = model.VisibilityLevel(*req.FollowersVisibility)
} else {
settings.FollowersVisibility = model.VisibilityEveryone
}
if req.FollowingVisibility != nil {
settings.FollowingVisibility = model.VisibilityLevel(*req.FollowingVisibility)
} else {
settings.FollowingVisibility = model.VisibilityEveryone
}
if req.PostsVisibility != nil {
settings.PostsVisibility = model.VisibilityLevel(*req.PostsVisibility)
} else {
settings.PostsVisibility = model.VisibilityEveryone
}
if req.FavoritesVisibility != nil {
settings.FavoritesVisibility = model.VisibilityLevel(*req.FavoritesVisibility)
} else {
settings.FavoritesVisibility = model.VisibilityEveryone
}
if err := h.userService.UpdatePrivacySettings(c.Request.Context(), userID, settings); err != nil {
response.InternalServerError(c, "failed to update privacy settings")
return
}
response.Success(c, &dto.PrivacySettingsResponse{
FollowersVisibility: string(settings.FollowersVisibility),
FollowingVisibility: string(settings.FollowingVisibility),
PostsVisibility: string(settings.PostsVisibility),
FavoritesVisibility: string(settings.FavoritesVisibility),
})
}
// RequestAccountDeletion 申请注销账号
func (h *UserHandler) RequestAccountDeletion(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
var req dto.RequestDeletionRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.userService.RequestAccountDeletion(c.Request.Context(), userID, req.Password); err != nil {
response.HandleError(c, err, "failed to request account deletion")
return
}
isPending, requestedAt, daysLeft, err := h.userService.GetDeletionStatus(c.Request.Context(), userID)
if err != nil {
response.InternalServerError(c, "failed to get deletion status")
return
}
resp := &dto.DeletionStatusResponse{
IsPendingDeletion: isPending,
CoolDownDays: daysLeft,
}
if requestedAt != nil {
resp.DeletionRequestedAt = dto.FormatTime(*requestedAt)
}
response.Success(c, resp)
}
// CancelAccountDeletion 取消注销
func (h *UserHandler) CancelAccountDeletion(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
if err := h.userService.CancelAccountDeletion(c.Request.Context(), userID); err != nil {
response.InternalServerError(c, "failed to cancel account deletion")
return
}
response.Success(c, gin.H{"success": true})
}
// GetDeletionStatus 获取注销状态
func (h *UserHandler) GetDeletionStatus(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
isPending, requestedAt, daysLeft, err := h.userService.GetDeletionStatus(c.Request.Context(), userID)
if err != nil {
response.InternalServerError(c, "failed to get deletion status")
return
}
resp := &dto.DeletionStatusResponse{
IsPendingDeletion: isPending,
CoolDownDays: daysLeft,
}
if requestedAt != nil {
resp.DeletionRequestedAt = dto.FormatTime(*requestedAt)
}
response.Success(c, resp)
}

View File

@@ -14,8 +14,26 @@ const (
UserStatusActive UserStatus = "active"
UserStatusBanned UserStatus = "banned"
UserStatusInactive UserStatus = "inactive"
UserStatusPendingDeletion UserStatus = "pending_deletion"
)
// VisibilityLevel 可见性级别
type VisibilityLevel string
const (
VisibilityEveryone VisibilityLevel = "everyone"
VisibilityFollowing VisibilityLevel = "following"
VisibilitySelf VisibilityLevel = "self"
)
// PrivacySettings 隐私设置
type PrivacySettings struct {
FollowersVisibility VisibilityLevel `json:"followers_visibility" gorm:"type:varchar(20);default:everyone"`
FollowingVisibility VisibilityLevel `json:"following_visibility" gorm:"type:varchar(20);default:everyone"`
PostsVisibility VisibilityLevel `json:"posts_visibility" gorm:"type:varchar(20);default:everyone"`
FavoritesVisibility VisibilityLevel `json:"favorites_visibility" gorm:"type:varchar(20);default:everyone"`
}
// UserIdentity 用户身份类型
type UserIdentity string
@@ -73,6 +91,12 @@ type User struct {
LastLoginAt *time.Time `json:"last_login_at" gorm:"type:timestamp"`
LastLoginIP string `json:"last_login_ip" gorm:"type:varchar(45)"`
// 账号注销
DeletionRequestedAt *time.Time `json:"deletion_requested_at" gorm:"type:timestamp"`
// 隐私设置
PrivacySettings PrivacySettings `json:"privacy_settings" gorm:"embedded;embeddedPrefix:privacy_"`
// 时间戳
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`

View File

@@ -3,6 +3,7 @@ package repository
import (
"carrot_bbs/internal/model"
"strings"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@@ -43,6 +44,16 @@ type UserRepository interface {
AdminList(page, pageSize int, keyword, status string) ([]*model.User, int64, error)
GetCommentsCount(userID string) (int64, error)
UpdateStatus(userID string, status model.UserStatus) error
// 隐私设置
UpdatePrivacySettings(userID string, settings *model.PrivacySettings) error
GetPrivacySettings(userID string) (*model.PrivacySettings, error)
// 账号注销
RequestDeletion(userID string) error
CancelDeletion(userID string) error
GetPendingDeletionUsers(beforeTime time.Time) ([]*model.User, error)
HardDeleteUser(userID string) error
}
// userRepository 用户仓储实现
@@ -544,3 +555,54 @@ func (r *userRepository) GetCommentsCount(userID string) (int64, error) {
func (r *userRepository) UpdateStatus(userID string, status model.UserStatus) error {
return r.db.Model(&model.User{}).Where("id = ?", userID).Update("status", status).Error
}
// UpdatePrivacySettings 更新隐私设置
func (r *userRepository) UpdatePrivacySettings(userID string, settings *model.PrivacySettings) error {
return r.db.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]interface{}{
"privacy_followers_visibility": settings.FollowersVisibility,
"privacy_following_visibility": settings.FollowingVisibility,
"privacy_posts_visibility": settings.PostsVisibility,
"privacy_favorites_visibility": settings.FavoritesVisibility,
}).Error
}
// GetPrivacySettings 获取隐私设置
func (r *userRepository) GetPrivacySettings(userID string) (*model.PrivacySettings, error) {
var user model.User
err := r.db.Select("privacy_followers_visibility, privacy_following_visibility, privacy_posts_visibility, privacy_favorites_visibility").
First(&user, "id = ?", userID).Error
if err != nil {
return nil, err
}
return &user.PrivacySettings, nil
}
// RequestDeletion 申请注销账号
func (r *userRepository) RequestDeletion(userID string) error {
now := time.Now()
return r.db.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]interface{}{
"deletion_requested_at": &now,
"status": model.UserStatusPendingDeletion,
}).Error
}
// CancelDeletion 取消注销
func (r *userRepository) CancelDeletion(userID string) error {
return r.db.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]interface{}{
"deletion_requested_at": nil,
"status": model.UserStatusActive,
}).Error
}
// GetPendingDeletionUsers 获取待删除的用户列表(注销申请时间早于指定时间)
func (r *userRepository) GetPendingDeletionUsers(beforeTime time.Time) ([]*model.User, error) {
var users []*model.User
err := r.db.Where("deletion_requested_at IS NOT NULL AND deletion_requested_at < ?", beforeTime).
Find(&users).Error
return users, err
}
// HardDeleteUser 硬删除用户(永久删除)
func (r *userRepository) HardDeleteUser(userID string) error {
return r.db.Unscoped().Delete(&model.User{}, "id = ?", userID).Error
}

View File

@@ -187,6 +187,15 @@ func (r *Router) setupRoutes() {
users.POST("/change-password/send-code", authMiddleware, r.userHandler.SendChangePasswordCode)
users.POST("/change-password", authMiddleware, r.userHandler.ChangePassword)
// 隐私设置
users.GET("/me/privacy", authMiddleware, r.userHandler.GetPrivacySettings)
users.PUT("/me/privacy", authMiddleware, r.userHandler.UpdatePrivacySettings)
// 账号注销
users.POST("/me/deactivate", authMiddleware, r.userHandler.RequestAccountDeletion)
users.DELETE("/me/deactivate", authMiddleware, r.userHandler.CancelAccountDeletion)
users.GET("/me/deletion-status", authMiddleware, r.userHandler.GetDeletionStatus)
// 当前用户角色
if r.roleHandler != nil {
users.GET("/me/roles", authMiddleware, r.roleHandler.GetMyRoles)

View File

@@ -0,0 +1,109 @@
package service
import (
"context"
"log"
"time"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
"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 {
log.Printf("[AccountCleanup] failed to cleanup user %s: %v", user.ID, err)
continue
}
deletedCount++
}
log.Printf("[AccountCleanup] cleaned up %d expired accounts", 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
})
}

View File

@@ -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