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

@@ -40,22 +40,35 @@ func ConvertUserToSelfResponse(user *model.User, postsCount int) *UserSelfRespon
return nil
}
return &UserSelfResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: postsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
IsVerified: user.IsVerified,
CreatedAt: FormatTime(user.CreatedAt),
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: postsCount,
FollowersCount: user.FollowersCount,
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),
}
}
@@ -159,21 +172,22 @@ func ConvertUserToDetailResponse(user *model.User) *UserDetailResponse {
return nil
}
return &UserDetailResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: user.PostsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
IsVerified: user.IsVerified,
CreatedAt: FormatTime(user.CreatedAt),
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: user.PostsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
IsVerified: user.IsVerified,
CreatedAt: FormatTime(user.CreatedAt),
PrivacySettings: convertPrivacySettingsToResponse(&user.PrivacySettings),
}
}
@@ -183,22 +197,23 @@ func ConvertUserToDetailResponseWithPostsCount(user *model.User, postsCount int)
return nil
}
return &UserDetailResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
EmailVerified: user.EmailVerified,
Phone: user.Phone,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: postsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
IsVerified: user.IsVerified,
CreatedAt: FormatTime(user.CreatedAt),
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
EmailVerified: user.EmailVerified,
Phone: user.Phone,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: postsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
IsVerified: user.IsVerified,
CreatedAt: FormatTime(user.CreatedAt),
PrivacySettings: convertPrivacySettingsToResponse(&user.PrivacySettings),
}
}