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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
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"
|
||||
"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