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:
@@ -44,6 +44,8 @@ type UserSelfResponse struct {
|
|||||||
FollowingCount int `json:"following_count"`
|
FollowingCount int `json:"following_count"`
|
||||||
IsVerified bool `json:"is_verified"`
|
IsVerified bool `json:"is_verified"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
|
// 隐私设置
|
||||||
|
PrivacySettings *PrivacySettingsResponse `json:"privacy_settings,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserDetailResponse 用户详情响应(仅本人可见,包含敏感信息)
|
// UserDetailResponse 用户详情响应(仅本人可见,包含敏感信息)
|
||||||
@@ -66,6 +68,8 @@ type UserDetailResponse struct {
|
|||||||
IsFollowing bool `json:"is_following"`
|
IsFollowing bool `json:"is_following"`
|
||||||
IsFollowingMe bool `json:"is_following_me"`
|
IsFollowingMe bool `json:"is_following_me"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
|
// 隐私设置
|
||||||
|
PrivacySettings *PrivacySettingsResponse `json:"privacy_settings,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Admin User DTOs ====================
|
// ==================== Admin User DTOs ====================
|
||||||
@@ -1403,3 +1407,33 @@ type AdminReviewVerificationRequest struct {
|
|||||||
Approve bool `json:"approve" binding:"required"`
|
Approve bool `json:"approve" binding:"required"`
|
||||||
RejectReason string `json:"reject_reason" binding:"omitempty,max=500"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,22 +40,35 @@ func ConvertUserToSelfResponse(user *model.User, postsCount int) *UserSelfRespon
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &UserSelfResponse{
|
return &UserSelfResponse{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Nickname: user.Nickname,
|
Nickname: user.Nickname,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Phone: user.Phone,
|
Phone: user.Phone,
|
||||||
EmailVerified: user.EmailVerified,
|
EmailVerified: user.EmailVerified,
|
||||||
Avatar: getAvatarOrDefault(user),
|
Avatar: getAvatarOrDefault(user),
|
||||||
CoverURL: user.CoverURL,
|
CoverURL: user.CoverURL,
|
||||||
Bio: user.Bio,
|
Bio: user.Bio,
|
||||||
Website: user.Website,
|
Website: user.Website,
|
||||||
Location: user.Location,
|
Location: user.Location,
|
||||||
PostsCount: postsCount,
|
PostsCount: postsCount,
|
||||||
FollowersCount: user.FollowersCount,
|
FollowersCount: user.FollowersCount,
|
||||||
FollowingCount: user.FollowingCount,
|
FollowingCount: user.FollowingCount,
|
||||||
IsVerified: user.IsVerified,
|
IsVerified: user.IsVerified,
|
||||||
CreatedAt: FormatTime(user.CreatedAt),
|
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 nil
|
||||||
}
|
}
|
||||||
return &UserDetailResponse{
|
return &UserDetailResponse{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Nickname: user.Nickname,
|
Nickname: user.Nickname,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
EmailVerified: user.EmailVerified,
|
EmailVerified: user.EmailVerified,
|
||||||
Avatar: getAvatarOrDefault(user),
|
Avatar: getAvatarOrDefault(user),
|
||||||
CoverURL: user.CoverURL,
|
CoverURL: user.CoverURL,
|
||||||
Bio: user.Bio,
|
Bio: user.Bio,
|
||||||
Website: user.Website,
|
Website: user.Website,
|
||||||
Location: user.Location,
|
Location: user.Location,
|
||||||
PostsCount: user.PostsCount,
|
PostsCount: user.PostsCount,
|
||||||
FollowersCount: user.FollowersCount,
|
FollowersCount: user.FollowersCount,
|
||||||
FollowingCount: user.FollowingCount,
|
FollowingCount: user.FollowingCount,
|
||||||
IsVerified: user.IsVerified,
|
IsVerified: user.IsVerified,
|
||||||
CreatedAt: FormatTime(user.CreatedAt),
|
CreatedAt: FormatTime(user.CreatedAt),
|
||||||
|
PrivacySettings: convertPrivacySettingsToResponse(&user.PrivacySettings),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,22 +197,23 @@ func ConvertUserToDetailResponseWithPostsCount(user *model.User, postsCount int)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &UserDetailResponse{
|
return &UserDetailResponse{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Nickname: user.Nickname,
|
Nickname: user.Nickname,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
EmailVerified: user.EmailVerified,
|
EmailVerified: user.EmailVerified,
|
||||||
Phone: user.Phone,
|
Phone: user.Phone,
|
||||||
Avatar: getAvatarOrDefault(user),
|
Avatar: getAvatarOrDefault(user),
|
||||||
CoverURL: user.CoverURL,
|
CoverURL: user.CoverURL,
|
||||||
Bio: user.Bio,
|
Bio: user.Bio,
|
||||||
Website: user.Website,
|
Website: user.Website,
|
||||||
Location: user.Location,
|
Location: user.Location,
|
||||||
PostsCount: postsCount,
|
PostsCount: postsCount,
|
||||||
FollowersCount: user.FollowersCount,
|
FollowersCount: user.FollowersCount,
|
||||||
FollowingCount: user.FollowingCount,
|
FollowingCount: user.FollowingCount,
|
||||||
IsVerified: user.IsVerified,
|
IsVerified: user.IsVerified,
|
||||||
CreatedAt: FormatTime(user.CreatedAt),
|
CreatedAt: FormatTime(user.CreatedAt),
|
||||||
|
PrivacySettings: convertPrivacySettingsToResponse(&user.PrivacySettings),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import (
|
|||||||
|
|
||||||
// PostHandler 帖子处理器
|
// PostHandler 帖子处理器
|
||||||
type PostHandler struct {
|
type PostHandler struct {
|
||||||
postService service.PostService
|
postService service.PostService
|
||||||
userService service.UserService
|
userService service.UserService
|
||||||
channelService service.ChannelService
|
channelService service.ChannelService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPostHandler 创建帖子处理器
|
// NewPostHandler 创建帖子处理器
|
||||||
@@ -116,27 +116,27 @@ func (h *PostHandler) GetByID(c *gin.Context) {
|
|||||||
|
|
||||||
// 构建响应
|
// 构建响应
|
||||||
responseData := &dto.PostResponse{
|
responseData := &dto.PostResponse{
|
||||||
ID: post.ID,
|
ID: post.ID,
|
||||||
UserID: post.UserID,
|
UserID: post.UserID,
|
||||||
ChannelID: post.ChannelID,
|
ChannelID: post.ChannelID,
|
||||||
Title: post.Title,
|
Title: post.Title,
|
||||||
Content: post.Content,
|
Content: post.Content,
|
||||||
Images: dto.ConvertPostImagesToResponse(post.Images),
|
Images: dto.ConvertPostImagesToResponse(post.Images),
|
||||||
Status: string(post.Status),
|
Status: string(post.Status),
|
||||||
LikesCount: post.LikesCount,
|
LikesCount: post.LikesCount,
|
||||||
CommentsCount: post.CommentsCount,
|
CommentsCount: post.CommentsCount,
|
||||||
FavoritesCount: post.FavoritesCount,
|
FavoritesCount: post.FavoritesCount,
|
||||||
SharesCount: post.SharesCount,
|
SharesCount: post.SharesCount,
|
||||||
ViewsCount: post.ViewsCount,
|
ViewsCount: post.ViewsCount,
|
||||||
IsPinned: post.IsPinned,
|
IsPinned: post.IsPinned,
|
||||||
IsLocked: post.IsLocked,
|
IsLocked: post.IsLocked,
|
||||||
IsVote: post.IsVote,
|
IsVote: post.IsVote,
|
||||||
CreatedAt: dto.FormatTime(post.CreatedAt),
|
CreatedAt: dto.FormatTime(post.CreatedAt),
|
||||||
UpdatedAt: dto.FormatTime(post.UpdatedAt),
|
UpdatedAt: dto.FormatTime(post.UpdatedAt),
|
||||||
ContentEditedAt: dto.FormatTimePointer(post.ContentEditedAt),
|
ContentEditedAt: dto.FormatTimePointer(post.ContentEditedAt),
|
||||||
Author: authorWithFollowStatus,
|
Author: authorWithFollowStatus,
|
||||||
IsLiked: isLiked,
|
IsLiked: isLiked,
|
||||||
IsFavorited: isFavorited,
|
IsFavorited: isFavorited,
|
||||||
}
|
}
|
||||||
|
|
||||||
chMap := h.channelMapForPosts([]*model.Post{post})
|
chMap := h.channelMapForPosts([]*model.Post{post})
|
||||||
@@ -553,6 +553,19 @@ func (h *PostHandler) Unfavorite(c *gin.Context) {
|
|||||||
|
|
||||||
// GetUserPosts 获取用户帖子列表(支持游标分页和偏移分页)
|
// GetUserPosts 获取用户帖子列表(支持游标分页和偏移分页)
|
||||||
func (h *PostHandler) GetUserPosts(c *gin.Context) {
|
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")
|
cursorStr := c.Query("cursor")
|
||||||
if cursorStr != "" {
|
if cursorStr != "" {
|
||||||
@@ -561,12 +574,10 @@ func (h *PostHandler) GetUserPosts(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 偏移分页(向后兼容)
|
// 偏移分页(向后兼容)
|
||||||
userID := c.Param("id")
|
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||||
page, pageSize = normalizePagination(page, pageSize)
|
page, pageSize = normalizePagination(page, pageSize)
|
||||||
|
|
||||||
currentUserID := c.GetString("user_id")
|
|
||||||
includePending := currentUserID != "" && currentUserID == userID
|
includePending := currentUserID != "" && currentUserID == userID
|
||||||
posts, total, err := h.postService.GetUserPosts(c.Request.Context(), userID, page, pageSize, includePending)
|
posts, total, err := h.postService.GetUserPosts(c.Request.Context(), userID, page, pageSize, includePending)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -629,6 +640,18 @@ func (h *PostHandler) GetUserPostsByCursor(c *gin.Context) {
|
|||||||
// GetFavorites 获取收藏列表
|
// GetFavorites 获取收藏列表
|
||||||
func (h *PostHandler) GetFavorites(c *gin.Context) {
|
func (h *PostHandler) GetFavorites(c *gin.Context) {
|
||||||
userID := c.Param("id")
|
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"))
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||||
page, pageSize = normalizePagination(page, pageSize)
|
page, pageSize = normalizePagination(page, pageSize)
|
||||||
@@ -639,8 +662,6 @@ func (h *PostHandler) GetFavorites(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量获取交互状态
|
|
||||||
currentUserID := c.GetString("user_id")
|
|
||||||
postIDs := make([]string, len(posts))
|
postIDs := make([]string, len(posts))
|
||||||
for i, post := range posts {
|
for i, post := range posts {
|
||||||
postIDs[i] = post.ID
|
postIDs[i] = post.ID
|
||||||
|
|||||||
@@ -615,6 +615,17 @@ func (h *UserHandler) GetBlockStatus(c *gin.Context) {
|
|||||||
func (h *UserHandler) GetFollowingList(c *gin.Context) {
|
func (h *UserHandler) GetFollowingList(c *gin.Context) {
|
||||||
userID := c.Param("id")
|
userID := c.Param("id")
|
||||||
currentUserID := c.GetString("user_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")
|
page := c.DefaultQuery("page", "1")
|
||||||
pageSize := c.DefaultQuery("page_size", "20")
|
pageSize := c.DefaultQuery("page_size", "20")
|
||||||
|
|
||||||
@@ -654,6 +665,17 @@ func (h *UserHandler) GetFollowingList(c *gin.Context) {
|
|||||||
func (h *UserHandler) GetFollowersList(c *gin.Context) {
|
func (h *UserHandler) GetFollowersList(c *gin.Context) {
|
||||||
userID := c.Param("id")
|
userID := c.Param("id")
|
||||||
currentUserID := c.GetString("user_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")
|
page := c.DefaultQuery("page", "1")
|
||||||
pageSize := c.DefaultQuery("page_size", "20")
|
pageSize := c.DefaultQuery("page_size", "20")
|
||||||
|
|
||||||
@@ -810,3 +832,151 @@ func (h *UserHandler) Search(c *gin.Context) {
|
|||||||
|
|
||||||
response.Paginated(c, userResponses, total, page, pageSize)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,11 +11,29 @@ import (
|
|||||||
type UserStatus string
|
type UserStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UserStatusActive UserStatus = "active"
|
UserStatusActive UserStatus = "active"
|
||||||
UserStatusBanned UserStatus = "banned"
|
UserStatusBanned UserStatus = "banned"
|
||||||
UserStatusInactive UserStatus = "inactive"
|
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 用户身份类型
|
// UserIdentity 用户身份类型
|
||||||
type UserIdentity string
|
type UserIdentity string
|
||||||
|
|
||||||
@@ -73,6 +91,12 @@ type User struct {
|
|||||||
LastLoginAt *time.Time `json:"last_login_at" gorm:"type:timestamp"`
|
LastLoginAt *time.Time `json:"last_login_at" gorm:"type:timestamp"`
|
||||||
LastLoginIP string `json:"last_login_ip" gorm:"type:varchar(45)"`
|
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"`
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"carrot_bbs/internal/model"
|
"carrot_bbs/internal/model"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
@@ -43,6 +44,16 @@ type UserRepository interface {
|
|||||||
AdminList(page, pageSize int, keyword, status string) ([]*model.User, int64, error)
|
AdminList(page, pageSize int, keyword, status string) ([]*model.User, int64, error)
|
||||||
GetCommentsCount(userID string) (int64, error)
|
GetCommentsCount(userID string) (int64, error)
|
||||||
UpdateStatus(userID string, status model.UserStatus) 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 用户仓储实现
|
// userRepository 用户仓储实现
|
||||||
@@ -544,3 +555,54 @@ func (r *userRepository) GetCommentsCount(userID string) (int64, error) {
|
|||||||
func (r *userRepository) UpdateStatus(userID string, status model.UserStatus) error {
|
func (r *userRepository) UpdateStatus(userID string, status model.UserStatus) error {
|
||||||
return r.db.Model(&model.User{}).Where("id = ?", userID).Update("status", status).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
|
||||||
|
}
|
||||||
|
|||||||
@@ -187,6 +187,15 @@ func (r *Router) setupRoutes() {
|
|||||||
users.POST("/change-password/send-code", authMiddleware, r.userHandler.SendChangePasswordCode)
|
users.POST("/change-password/send-code", authMiddleware, r.userHandler.SendChangePasswordCode)
|
||||||
users.POST("/change-password", authMiddleware, r.userHandler.ChangePassword)
|
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 {
|
if r.roleHandler != nil {
|
||||||
users.GET("/me/roles", authMiddleware, r.roleHandler.GetMyRoles)
|
users.GET("/me/roles", authMiddleware, r.roleHandler.GetMyRoles)
|
||||||
|
|||||||
109
internal/service/account_cleanup_service.go
Normal file
109
internal/service/account_cleanup_service.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"carrot_bbs/internal/cache"
|
"carrot_bbs/internal/cache"
|
||||||
apperrors "carrot_bbs/internal/errors"
|
apperrors "carrot_bbs/internal/errors"
|
||||||
@@ -52,6 +53,20 @@ type UserService interface {
|
|||||||
UnblockUser(ctx context.Context, blockerID, blockedID string) error
|
UnblockUser(ctx context.Context, blockerID, blockedID string) error
|
||||||
GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error)
|
GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error)
|
||||||
IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, 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 用户服务实现
|
// userServiceImpl 用户服务实现
|
||||||
@@ -320,11 +335,10 @@ func (s *userServiceImpl) Login(ctx context.Context, account, password string) (
|
|||||||
return nil, ErrInvalidCredentials
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Status != model.UserStatusActive {
|
if user.Status != model.UserStatusActive && user.Status != model.UserStatusPendingDeletion {
|
||||||
loginResult = string(model.LoginResultFail)
|
loginResult = string(model.LoginResultFail)
|
||||||
failReason = string(model.FailReasonAccountBanned)
|
failReason = string(model.FailReasonAccountBanned)
|
||||||
|
|
||||||
// 记录失败登录日志
|
|
||||||
if s.logService != nil {
|
if s.logService != nil {
|
||||||
s.logService.LoginLog.RecordLogin(&model.LoginLog{
|
s.logService.LoginLog.RecordLogin(&model.LoginLog{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
@@ -341,6 +355,13 @@ func (s *userServiceImpl) Login(ctx context.Context, account, password string) (
|
|||||||
return nil, ErrUserBanned
|
return nil, ErrUserBanned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果用户已申请注销,登录即取消注销
|
||||||
|
if user.DeletionRequestedAt != nil {
|
||||||
|
_ = s.userRepo.CancelDeletion(user.ID)
|
||||||
|
user.DeletionRequestedAt = nil
|
||||||
|
user.Status = model.UserStatusActive
|
||||||
|
}
|
||||||
|
|
||||||
// 记录成功登录日志
|
// 记录成功登录日志
|
||||||
if s.logService != nil {
|
if s.logService != nil {
|
||||||
s.logService.LoginLog.RecordLogin(&model.LoginLog{
|
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)
|
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
|
// 错误定义 - 使用统一的 AppError
|
||||||
var (
|
var (
|
||||||
ErrInvalidUsername = apperrors.ErrInvalidUsername
|
ErrInvalidUsername = apperrors.ErrInvalidUsername
|
||||||
|
|||||||
Reference in New Issue
Block a user