Implement sensitive word filtering feature with configurable database and Redis support. Add security enhancements including WebSocket origin validation, improved password strength requirements, timing attack prevention, and production JWT secret validation. Add batch operation validation limits and optimize user blocking queries with subqueries. BREAKING CHANGE: Password validation now requires minimum 8 characters with uppercase, lowercase, and digit (was 6-50 characters without complexity requirements)
547 lines
17 KiB
Go
547 lines
17 KiB
Go
package repository
|
||
|
||
import (
|
||
"carrot_bbs/internal/model"
|
||
"strings"
|
||
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
// UserRepository 用户仓储接口
|
||
type UserRepository interface {
|
||
Create(user *model.User) error
|
||
GetByID(id string) (*model.User, error)
|
||
GetByUsername(username string) (*model.User, error)
|
||
GetByEmail(email string) (*model.User, error)
|
||
GetByPhone(phone string) (*model.User, error)
|
||
Update(user *model.User) error
|
||
Delete(id string) error
|
||
List(page, pageSize int) ([]*model.User, int64, error)
|
||
GetFollowers(userID string, page, pageSize int) ([]*model.User, int64, error)
|
||
GetFollowing(userID string, page, pageSize int) ([]*model.User, int64, error)
|
||
CreateFollow(follow *model.Follow) error
|
||
DeleteFollow(followerID, followingID string) error
|
||
IsFollowing(followerID, followingID string) (bool, error)
|
||
IsFollowingBatch(followerID string, targetUserIDs []string) (map[string]bool, error)
|
||
IncrementFollowersCount(userID string) error
|
||
DecrementFollowersCount(userID string) error
|
||
IncrementFollowingCount(userID string) error
|
||
DecrementFollowingCount(userID string) error
|
||
RefreshFollowersCount(userID string) error
|
||
GetPostsCount(userID string) (int64, error)
|
||
GetPostsCountBatch(userIDs []string) (map[string]int64, error)
|
||
RefreshFollowingCount(userID string) error
|
||
IsBlocked(blockerID, blockedID string) (bool, error)
|
||
IsBlockedBatch(blockerID string, blockedIDs []string) (map[string]bool, error)
|
||
IsBlockedEitherDirection(userA, userB string) (bool, error)
|
||
BlockUserAndCleanupRelations(blockerID, blockedID string) error
|
||
UnblockUser(blockerID, blockedID string) error
|
||
GetBlockedUsers(blockerID string, page, pageSize int) ([]*model.User, int64, error)
|
||
Search(keyword string, page, pageSize int) ([]*model.User, int64, error)
|
||
GetMutualFollowStatus(currentUserID string, targetUserIDs []string) (map[string][2]bool, error)
|
||
AdminList(page, pageSize int, keyword, status string) ([]*model.User, int64, error)
|
||
GetCommentsCount(userID string) (int64, error)
|
||
UpdateStatus(userID string, status model.UserStatus) error
|
||
}
|
||
|
||
// userRepository 用户仓储实现
|
||
type userRepository struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
// NewUserRepository 创建用户仓储
|
||
func NewUserRepository(db *gorm.DB) UserRepository {
|
||
return &userRepository{db: db}
|
||
}
|
||
|
||
// Create 创建用户
|
||
func (r *userRepository) Create(user *model.User) error {
|
||
return r.db.Create(user).Error
|
||
}
|
||
|
||
// GetByID 根据ID获取用户
|
||
func (r *userRepository) GetByID(id string) (*model.User, error) {
|
||
var user model.User
|
||
err := r.db.First(&user, "id = ?", id).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &user, nil
|
||
}
|
||
|
||
// GetByUsername 根据用户名获取用户
|
||
func (r *userRepository) GetByUsername(username string) (*model.User, error) {
|
||
var user model.User
|
||
err := r.db.First(&user, "username = ?", username).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &user, nil
|
||
}
|
||
|
||
// GetByEmail 根据邮箱获取用户
|
||
func (r *userRepository) GetByEmail(email string) (*model.User, error) {
|
||
var user model.User
|
||
err := r.db.First(&user, "email = ?", email).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &user, nil
|
||
}
|
||
|
||
// GetByPhone 根据手机号获取用户
|
||
func (r *userRepository) GetByPhone(phone string) (*model.User, error) {
|
||
var user model.User
|
||
err := r.db.First(&user, "phone = ?", phone).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &user, nil
|
||
}
|
||
|
||
// Update 更新用户
|
||
func (r *userRepository) Update(user *model.User) error {
|
||
return r.db.Save(user).Error
|
||
}
|
||
|
||
// Delete 删除用户
|
||
func (r *userRepository) Delete(id string) error {
|
||
return r.db.Delete(&model.User{}, "id = ?", id).Error
|
||
}
|
||
|
||
// List 分页获取用户列表
|
||
func (r *userRepository) List(page, pageSize int) ([]*model.User, int64, error) {
|
||
var users []*model.User
|
||
var total int64
|
||
|
||
r.db.Model(&model.User{}).Count(&total)
|
||
|
||
offset := (page - 1) * pageSize
|
||
err := r.db.Order("created_at DESC, id DESC").Offset(offset).Limit(pageSize).Find(&users).Error
|
||
|
||
return users, total, err
|
||
}
|
||
|
||
// GetFollowers 获取用户粉丝
|
||
func (r *userRepository) GetFollowers(userID string, page, pageSize int) ([]*model.User, int64, error) {
|
||
var users []*model.User
|
||
var total int64
|
||
|
||
subQuery := r.db.Model(&model.Follow{}).Where("following_id = ?", userID).Select("follower_id")
|
||
r.db.Model(&model.User{}).Where("id IN (?)", subQuery).Count(&total)
|
||
|
||
offset := (page - 1) * pageSize
|
||
err := r.db.Where("id IN (?)", subQuery).
|
||
Order("created_at DESC, id DESC").
|
||
Offset(offset).Limit(pageSize).
|
||
Find(&users).Error
|
||
|
||
return users, total, err
|
||
}
|
||
|
||
// GetFollowing 获取用户关注
|
||
func (r *userRepository) GetFollowing(userID string, page, pageSize int) ([]*model.User, int64, error) {
|
||
var users []*model.User
|
||
var total int64
|
||
|
||
subQuery := r.db.Model(&model.Follow{}).Where("follower_id = ?", userID).Select("following_id")
|
||
r.db.Model(&model.User{}).Where("id IN (?)", subQuery).Count(&total)
|
||
|
||
offset := (page - 1) * pageSize
|
||
err := r.db.Where("id IN (?)", subQuery).
|
||
Order("created_at DESC, id DESC").
|
||
Offset(offset).Limit(pageSize).
|
||
Find(&users).Error
|
||
|
||
return users, total, err
|
||
}
|
||
|
||
// CreateFollow 创建关注关系
|
||
func (r *userRepository) CreateFollow(follow *model.Follow) error {
|
||
return r.db.Create(follow).Error
|
||
}
|
||
|
||
// DeleteFollow 删除关注关系
|
||
func (r *userRepository) DeleteFollow(followerID, followingID string) error {
|
||
return r.db.Where("follower_id = ? AND following_id = ?", followerID, followingID).Delete(&model.Follow{}).Error
|
||
}
|
||
|
||
// IsFollowing 检查是否关注了某用户
|
||
func (r *userRepository) IsFollowing(followerID, followingID string) (bool, error) {
|
||
var count int64
|
||
err := r.db.Model(&model.Follow{}).Where("follower_id = ? AND following_id = ?", followerID, followingID).Count(&count).Error
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return count > 0, nil
|
||
}
|
||
|
||
// IsFollowingBatch 批量检查当前用户是否关注了目标用户(解决 N+1 问题)
|
||
// 返回 map[targetUserID]bool
|
||
func (r *userRepository) IsFollowingBatch(followerID string, targetUserIDs []string) (map[string]bool, error) {
|
||
result := make(map[string]bool)
|
||
if len(targetUserIDs) == 0 || followerID == "" {
|
||
return result, nil
|
||
}
|
||
|
||
// 初始化所有目标用户为 false
|
||
for _, id := range targetUserIDs {
|
||
result[id] = false
|
||
}
|
||
|
||
// 一次性查询所有关注记录
|
||
var followingIDs []string
|
||
err := r.db.Model(&model.Follow{}).
|
||
Where("follower_id = ? AND following_id IN ?", followerID, targetUserIDs).
|
||
Pluck("following_id", &followingIDs).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 更新已关注的用户
|
||
for _, id := range followingIDs {
|
||
result[id] = true
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// IncrementFollowersCount 增加用户粉丝数
|
||
func (r *userRepository) IncrementFollowersCount(userID string) error {
|
||
return r.db.Model(&model.User{}).Where("id = ?", userID).
|
||
UpdateColumn("followers_count", gorm.Expr("followers_count + 1")).Error
|
||
}
|
||
|
||
// DecrementFollowersCount 减少用户粉丝数
|
||
func (r *userRepository) DecrementFollowersCount(userID string) error {
|
||
return r.db.Model(&model.User{}).Where("id = ? AND followers_count > 0", userID).
|
||
UpdateColumn("followers_count", gorm.Expr("followers_count - 1")).Error
|
||
}
|
||
|
||
// IncrementFollowingCount 增加用户关注数
|
||
func (r *userRepository) IncrementFollowingCount(userID string) error {
|
||
return r.db.Model(&model.User{}).Where("id = ?", userID).
|
||
UpdateColumn("following_count", gorm.Expr("following_count + 1")).Error
|
||
}
|
||
|
||
// DecrementFollowingCount 减少用户关注数
|
||
func (r *userRepository) DecrementFollowingCount(userID string) error {
|
||
return r.db.Model(&model.User{}).Where("id = ? AND following_count > 0", userID).
|
||
UpdateColumn("following_count", gorm.Expr("following_count - 1")).Error
|
||
}
|
||
|
||
// RefreshFollowersCount 刷新用户粉丝数(通过实际计数)
|
||
func (r *userRepository) RefreshFollowersCount(userID string) error {
|
||
var count int64
|
||
err := r.db.Model(&model.Follow{}).Where("following_id = ?", userID).Count(&count).Error
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return r.db.Model(&model.User{}).Where("id = ?", userID).
|
||
UpdateColumn("followers_count", count).Error
|
||
}
|
||
|
||
// GetPostsCount 获取用户帖子数(实时计算)
|
||
func (r *userRepository) GetPostsCount(userID string) (int64, error) {
|
||
var count int64
|
||
err := r.db.Model(&model.Post{}).
|
||
Where("user_id = ? AND status = ?", userID, model.PostStatusPublished).
|
||
Count(&count).Error
|
||
return count, err
|
||
}
|
||
|
||
// GetPostsCountBatch 批量获取用户帖子数(实时计算)
|
||
// 返回 map[userID]postsCount
|
||
func (r *userRepository) GetPostsCountBatch(userIDs []string) (map[string]int64, error) {
|
||
result := make(map[string]int64)
|
||
if len(userIDs) == 0 {
|
||
return result, nil
|
||
}
|
||
|
||
// 初始化所有用户ID的计数为0
|
||
for _, userID := range userIDs {
|
||
result[userID] = 0
|
||
}
|
||
|
||
// 使用 GROUP BY 一次性查询所有用户的帖子数
|
||
type CountResult struct {
|
||
UserID string
|
||
Count int64
|
||
}
|
||
var counts []CountResult
|
||
err := r.db.Model(&model.Post{}).
|
||
Select("user_id, count(*) as count").
|
||
Where("user_id IN ? AND status = ?", userIDs, model.PostStatusPublished).
|
||
Group("user_id").
|
||
Scan(&counts).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 更新查询结果
|
||
for _, c := range counts {
|
||
result[c.UserID] = c.Count
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// RefreshFollowingCount 刷新用户关注数(通过实际计数)
|
||
func (r *userRepository) RefreshFollowingCount(userID string) error {
|
||
var count int64
|
||
err := r.db.Model(&model.Follow{}).Where("follower_id = ?", userID).Count(&count).Error
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return r.db.Model(&model.User{}).Where("id = ?", userID).
|
||
UpdateColumn("following_count", count).Error
|
||
}
|
||
|
||
// IsBlocked 检查拉黑关系是否存在(blocker -> blocked)
|
||
func (r *userRepository) IsBlocked(blockerID, blockedID string) (bool, error) {
|
||
var count int64
|
||
err := r.db.Model(&model.UserBlock{}).
|
||
Where("blocker_id = ? AND blocked_id = ?", blockerID, blockedID).
|
||
Count(&count).Error
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return count > 0, nil
|
||
}
|
||
|
||
// IsBlockedBatch 批量检查拉黑关系(解决 N+1 问题)
|
||
// 返回 map[blockedUserID]bool
|
||
func (r *userRepository) IsBlockedBatch(blockerID string, blockedIDs []string) (map[string]bool, error) {
|
||
result := make(map[string]bool)
|
||
if len(blockedIDs) == 0 || blockerID == "" {
|
||
return result, nil
|
||
}
|
||
|
||
// 初始化所有目标用户为 false
|
||
for _, id := range blockedIDs {
|
||
result[id] = false
|
||
}
|
||
|
||
// 一次性查询所有拉黑记录
|
||
var blockedUserIDs []string
|
||
err := r.db.Model(&model.UserBlock{}).
|
||
Where("blocker_id = ? AND blocked_id IN ?", blockerID, blockedIDs).
|
||
Pluck("blocked_id", &blockedUserIDs).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 更新已拉黑的用户
|
||
for _, id := range blockedUserIDs {
|
||
result[id] = true
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// IsBlockedEitherDirection 检查是否任一方向存在拉黑
|
||
func (r *userRepository) IsBlockedEitherDirection(userA, userB string) (bool, error) {
|
||
var count int64
|
||
err := r.db.Model(&model.UserBlock{}).
|
||
Where("(blocker_id = ? AND blocked_id = ?) OR (blocker_id = ? AND blocked_id = ?)",
|
||
userA, userB, userB, userA).
|
||
Count(&count).Error
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return count > 0, nil
|
||
}
|
||
|
||
// BlockUserAndCleanupRelations 拉黑用户并清理双向关注关系(事务)
|
||
func (r *userRepository) BlockUserAndCleanupRelations(blockerID, blockedID string) error {
|
||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||
block := &model.UserBlock{
|
||
BlockerID: blockerID,
|
||
BlockedID: blockedID,
|
||
}
|
||
if err := tx.Clauses(clause.OnConflict{
|
||
Columns: []clause.Column{{Name: "blocker_id"}, {Name: "blocked_id"}},
|
||
DoNothing: true,
|
||
}).Create(block).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := tx.Where("follower_id = ? AND following_id = ?", blockerID, blockedID).
|
||
Delete(&model.Follow{}).Error; err != nil {
|
||
return err
|
||
}
|
||
if err := tx.Where("follower_id = ? AND following_id = ?", blockedID, blockerID).
|
||
Delete(&model.Follow{}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
for _, uid := range []string{blockerID, blockedID} {
|
||
if err := tx.Model(&model.User{}).Where("id = ?", uid).
|
||
UpdateColumn("followers_count", tx.Model(&model.Follow{}).
|
||
Select("COUNT(*)").Where("following_id = ?", uid)).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := tx.Model(&model.User{}).Where("id = ?", uid).
|
||
UpdateColumn("following_count", tx.Model(&model.Follow{}).
|
||
Select("COUNT(*)").Where("follower_id = ?", uid)).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// UnblockUser 取消拉黑
|
||
func (r *userRepository) UnblockUser(blockerID, blockedID string) error {
|
||
return r.db.Where("blocker_id = ? AND blocked_id = ?", blockerID, blockedID).
|
||
Delete(&model.UserBlock{}).Error
|
||
}
|
||
|
||
// GetBlockedUsers 获取用户黑名单列表
|
||
func (r *userRepository) GetBlockedUsers(blockerID string, page, pageSize int) ([]*model.User, int64, error) {
|
||
var users []*model.User
|
||
var total int64
|
||
|
||
subQuery := r.db.Model(&model.UserBlock{}).Where("blocker_id = ?", blockerID).Select("blocked_id")
|
||
r.db.Model(&model.User{}).Where("id IN (?)", subQuery).Count(&total)
|
||
|
||
offset := (page - 1) * pageSize
|
||
err := r.db.Where("id IN (?)", subQuery).
|
||
Order("created_at DESC, id DESC").
|
||
Offset(offset).
|
||
Limit(pageSize).
|
||
Find(&users).Error
|
||
|
||
return users, total, err
|
||
}
|
||
|
||
// Search 搜索用户
|
||
func (r *userRepository) Search(keyword string, page, pageSize int) ([]*model.User, int64, error) {
|
||
var users []*model.User
|
||
var total int64
|
||
|
||
query := r.db.Model(&model.User{})
|
||
|
||
// 搜索用户名、昵称、简介
|
||
if keyword != "" {
|
||
if r.db.Dialector.Name() == "postgres" {
|
||
query = query.Where(
|
||
"to_tsvector('simple', COALESCE(username, '') || ' ' || COALESCE(nickname, '') || ' ' || COALESCE(bio, '')) @@ plainto_tsquery('simple', ?)",
|
||
keyword,
|
||
)
|
||
} else {
|
||
// SQLite: 使用 LOWER() 实现大小写不敏感的模糊搜索
|
||
lowerSearchPattern := "%" + strings.ToLower(keyword) + "%"
|
||
query = query.Where(
|
||
"LOWER(username) LIKE ? OR LOWER(nickname) LIKE ? OR LOWER(bio) LIKE ?",
|
||
lowerSearchPattern, lowerSearchPattern, lowerSearchPattern,
|
||
)
|
||
}
|
||
}
|
||
|
||
query.Count(&total)
|
||
|
||
offset := (page - 1) * pageSize
|
||
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&users).Error
|
||
|
||
return users, total, err
|
||
}
|
||
|
||
// GetMutualFollowStatus 批量获取双向关注状态
|
||
// 返回 map[userID][isFollowing, isFollowingMe]
|
||
func (r *userRepository) GetMutualFollowStatus(currentUserID string, targetUserIDs []string) (map[string][2]bool, error) {
|
||
result := make(map[string][2]bool)
|
||
|
||
if len(targetUserIDs) == 0 {
|
||
return result, nil
|
||
}
|
||
|
||
// 初始化所有目标用户为未关注状态
|
||
for _, userID := range targetUserIDs {
|
||
result[userID] = [2]bool{false, false}
|
||
}
|
||
|
||
// 查询当前用户关注了哪些目标用户 (isFollowing)
|
||
var followingIDs []string
|
||
err := r.db.Model(&model.Follow{}).
|
||
Where("follower_id = ? AND following_id IN ?", currentUserID, targetUserIDs).
|
||
Pluck("following_id", &followingIDs).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, id := range followingIDs {
|
||
status := result[id]
|
||
status[0] = true
|
||
result[id] = status
|
||
}
|
||
|
||
// 查询哪些目标用户关注了当前用户 (isFollowingMe)
|
||
var followerIDs []string
|
||
err = r.db.Model(&model.Follow{}).
|
||
Where("follower_id IN ? AND following_id = ?", targetUserIDs, currentUserID).
|
||
Pluck("follower_id", &followerIDs).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, id := range followerIDs {
|
||
status := result[id]
|
||
status[1] = true
|
||
result[id] = status
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// AdminList 管理端分页获取用户列表(支持关键词和状态筛选)
|
||
func (r *userRepository) AdminList(page, pageSize int, keyword, status string) ([]*model.User, int64, error) {
|
||
var users []*model.User
|
||
var total int64
|
||
|
||
query := r.db.Model(&model.User{})
|
||
|
||
// 关键词搜索(用户名、昵称、邮箱)
|
||
if keyword != "" {
|
||
if r.db.Dialector.Name() == "postgres" {
|
||
query = query.Where(
|
||
"username ILIKE ? OR nickname ILIKE ? OR email ILIKE ?",
|
||
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%",
|
||
)
|
||
} else {
|
||
lowerSearchPattern := "%" + strings.ToLower(keyword) + "%"
|
||
query = query.Where(
|
||
"LOWER(username) LIKE ? OR LOWER(nickname) LIKE ? OR LOWER(email) LIKE ?",
|
||
lowerSearchPattern, lowerSearchPattern, lowerSearchPattern,
|
||
)
|
||
}
|
||
}
|
||
|
||
// 状态筛选
|
||
if status != "" {
|
||
query = query.Where("status = ?", status)
|
||
}
|
||
|
||
query.Count(&total)
|
||
|
||
offset := (page - 1) * pageSize
|
||
err := query.Order("created_at DESC, id DESC").Offset(offset).Limit(pageSize).Find(&users).Error
|
||
|
||
return users, total, err
|
||
}
|
||
|
||
// GetCommentsCount 获取用户评论数
|
||
func (r *userRepository) GetCommentsCount(userID string) (int64, error) {
|
||
var count int64
|
||
err := r.db.Model(&model.Comment{}).
|
||
Where("user_id = ?", userID).
|
||
Count(&count).Error
|
||
return count, err
|
||
}
|
||
|
||
// UpdateStatus 更新用户状态
|
||
func (r *userRepository) UpdateStatus(userID string, status model.UserStatus) error {
|
||
return r.db.Model(&model.User{}).Where("id = ?", userID).Update("status", status).Error
|
||
}
|