feat(repository): enhance repository methods with batch processing and transaction support
All checks were successful
Build Backend / build (push) Successful in 12m50s
Build Backend / build-docker (push) Successful in 2m57s

- Updated Like and Unlike methods in CommentRepository to use transactions for data consistency.
- Introduced IsLikedBatch method for batch checking if comments are liked, addressing N+1 query issues.
- Enhanced BatchDelete method in PostRepository to perform batch deletions within a single transaction, improving efficiency.
- Added BatchUpdateStatus method for posts to utilize a single SQL update for status changes, reducing database load.
- Implemented BatchUpdateSortOrder in StickerRepository using CASE WHEN for efficient sorting updates.
- Added IsFollowingBatch and IsBlockedBatch methods in UserRepository for batch checking follow and block relationships, optimizing performance.
This commit is contained in:
lafay
2026-03-26 01:50:54 +08:00
parent 56bd971e42
commit 7b41dfeb00
9 changed files with 386 additions and 109 deletions

View File

@@ -255,11 +255,12 @@ func (r *CommentRepository) GetReplies(parentID string) ([]*model.Comment, error
return comments, err
}
// Like 点赞评论
// Like 点赞评论(使用事务保证数据一致性)
func (r *CommentRepository) Like(commentID, userID string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// 检查是否已经点赞
var existing model.CommentLike
err := r.db.Where("comment_id = ? AND user_id = ?", commentID, userID).First(&existing).Error
err := tx.Where("comment_id = ? AND user_id = ?", commentID, userID).First(&existing).Error
if err == nil {
// 已经点赞
return nil
@@ -273,28 +274,30 @@ func (r *CommentRepository) Like(commentID, userID string) error {
CommentID: commentID,
UserID: userID,
}
err = r.db.Create(like).Error
if err != nil {
if err := tx.Create(like).Error; err != nil {
return err
}
// 增加评论点赞数
return r.db.Model(&model.Comment{}).Where("id = ?", commentID).
return tx.Model(&model.Comment{}).Where("id = ?", commentID).
UpdateColumn("likes_count", gorm.Expr("likes_count + 1")).Error
})
}
// Unlike 取消点赞评论
// Unlike 取消点赞评论(使用事务保证数据一致性)
func (r *CommentRepository) Unlike(commentID, userID string) error {
result := r.db.Where("comment_id = ? AND user_id = ?", commentID, userID).Delete(&model.CommentLike{})
return r.db.Transaction(func(tx *gorm.DB) error {
result := tx.Where("comment_id = ? AND user_id = ?", commentID, userID).Delete(&model.CommentLike{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected > 0 {
// 减少评论点赞数
return r.db.Model(&model.Comment{}).Where("id = ?", commentID).
return tx.Model(&model.Comment{}).Where("id = ?", commentID).
UpdateColumn("likes_count", gorm.Expr("likes_count - 1")).Error
}
return nil
})
}
// IsLiked 检查是否已点赞
@@ -304,6 +307,33 @@ func (r *CommentRepository) IsLiked(commentID, userID string) bool {
return count > 0
}
// IsLikedBatch 批量检查是否点赞(解决 N+1 问题)
// 返回 map[commentID]bool
func (r *CommentRepository) IsLikedBatch(commentIDs []string, userID string) map[string]bool {
result := make(map[string]bool)
if len(commentIDs) == 0 || userID == "" {
return result
}
// 初始化所有 commentID 为 false
for _, commentID := range commentIDs {
result[commentID] = false
}
// 一次性查询所有点赞记录
var likedCommentIDs []string
r.db.Model(&model.CommentLike{}).
Where("comment_id IN ? AND user_id = ?", commentIDs, userID).
Pluck("comment_id", &likedCommentIDs)
// 更新已点赞的评论
for _, commentID := range likedCommentIDs {
result[commentID] = true
}
return result
}
// AdminCommentListFilter 管理端评论列表筛选条件
type AdminCommentListFilter struct {
Keyword string
@@ -367,17 +397,58 @@ func (r *CommentRepository) GetAdminCommentByID(id string) (*model.Comment, erro
return &comment, nil
}
// BatchDelete 批量删除评论
// BatchDelete 批量删除评论(使用单次事务批量删除,避免 N+1 问题)
func (r *CommentRepository) BatchDelete(ids []string) ([]string, error) {
var failedIDs []string
if len(ids) == 0 {
return nil, nil
}
for _, id := range ids {
if err := r.Delete(id); err != nil {
failedIDs = append(failedIDs, id)
// 先查询所有评论获取 post_id 和状态信息
type commentInfo struct {
ID string
PostID string
Status model.CommentStatus
}
var comments []commentInfo
if err := r.db.Model(&model.Comment{}).Where("id IN ?", ids).Scan(&comments).Error; err != nil {
return ids, err
}
err := r.db.Transaction(func(tx *gorm.DB) error {
// 批量删除评论点赞记录
if err := tx.Where("comment_id IN ?", ids).Delete(&model.CommentLike{}).Error; err != nil {
return err
}
// 批量删除评论(软删除)
if err := tx.Where("id IN ?", ids).Delete(&model.Comment{}).Error; err != nil {
return err
}
// 更新帖子的评论数(仅统计已发布评论)
postCountMap := make(map[string]int)
for _, c := range comments {
if c.Status == model.CommentStatusPublished {
postCountMap[c.PostID]++
}
}
for postID, count := range postCountMap {
if err := tx.Model(&model.Post{}).Where("id = ?", postID).
UpdateColumns(map[string]any{
"comments_count": gorm.Expr("comments_count - ?", count),
"updated_at": gorm.Expr("updated_at"),
}).Error; err != nil {
return err
}
}
return failedIDs, nil
return nil
})
if err != nil {
return ids, err
}
return nil, nil
}
// GetCommentsByPostIDForAdmin 管理端获取帖子的评论列表(包含所有状态)

View File

@@ -4,6 +4,7 @@ import (
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/cursor"
"context"
"errors"
"gorm.io/gorm"
)
@@ -188,6 +189,36 @@ func (r *groupRepository) IsMember(groupID string, userID string) (bool, error)
return count > 0, err
}
// GetExistingMembers 批量检查用户是否是群成员(解决 N+1 问题)
// 返回 map[userID]bool
func (r *groupRepository) GetExistingMembers(groupID string, userIDs []string) (map[string]bool, error) {
result := make(map[string]bool)
if len(userIDs) == 0 || groupID == "" {
return result, nil
}
// 初始化所有用户为 false
for _, id := range userIDs {
result[id] = false
}
// 一次性查询所有群成员记录
var existingIDs []string
err := r.db.Model(&model.GroupMember{}).
Where("group_id = ? AND user_id IN ?", groupID, userIDs).
Pluck("user_id", &existingIDs).Error
if err != nil {
return nil, err
}
// 更新已存在的成员
for _, id := range existingIDs {
result[id] = true
}
return result, nil
}
// GetUserGroups 获取用户加入的群组列表
func (r *groupRepository) GetUserGroups(userID string, page, pageSize int) ([]model.Group, int64, error) {
var groups []model.Group
@@ -318,7 +349,7 @@ func (r *groupRepository) GetLatestAnnouncement(groupID string) (*model.GroupAnn
Order("is_pinned DESC, created_at DESC").
First(&announcement).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err

View File

@@ -107,7 +107,7 @@ func (r *LoginLogRepository) DeleteOldLogs(ctx context.Context, beforeDate strin
return r.db.WithContext(ctx).Where("created_at < ?", beforeDate).Delete(&model.LoginLog{}).Error
}
// GetStatistics 获取登录日志统计
// GetStatistics 获取登录日志统计(使用单次 GROUP BY 查询,避免多次 COUNT
func (r *LoginLogRepository) GetStatistics(ctx context.Context, startTime, endTime string) (*LoginLogStatistics, error) {
stats := &LoginLogStatistics{}
@@ -119,20 +119,37 @@ func (r *LoginLogRepository) GetStatistics(ctx context.Context, startTime, endTi
query = query.Where("created_at <= ?", endTime)
}
if err := query.Count(&stats.TotalCount).Error; err != nil {
// 使用单次 GROUP BY 查询获取 result 计数
type resultCount struct {
Result string
Count int64
}
var results []resultCount
err := query.Select("result, count(*) as count").Group("result").Scan(&results).Error
if err != nil {
return nil, err
}
if err := query.Where("result = ?", string(model.LoginResultSuccess)).Count(&stats.SuccessCount).Error; err != nil {
return nil, err
// 汇总结果
for _, r := range results {
stats.TotalCount += r.Count
if r.Result == string(model.LoginResultSuccess) {
stats.SuccessCount = r.Count
} else if r.Result == string(model.LoginResultFail) {
stats.FailCount = r.Count
}
}
if err := query.Where("result = ?", string(model.LoginResultFail)).Count(&stats.FailCount).Error; err != nil {
return nil, err
// 单独查询登录次数
if startTime != "" || endTime != "" {
loginQuery := r.db.WithContext(ctx).Model(&model.LoginLog{})
if startTime != "" {
loginQuery = loginQuery.Where("created_at >= ?", startTime)
}
if err := query.Where("event = ?", string(model.LoginEventLogin)).Count(&stats.LoginCount).Error; err != nil {
return nil, err
if endTime != "" {
loginQuery = loginQuery.Where("created_at <= ?", endTime)
}
loginQuery.Where("event = ?", string(model.LoginEventLogin)).Count(&stats.LoginCount)
}
return stats, nil

View File

@@ -4,6 +4,7 @@ import (
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/cursor"
"context"
"errors"
"fmt"
"strconv"
"strings"
@@ -188,7 +189,7 @@ func (r *MessageRepository) GetParticipant(conversationID string, userID string)
err := r.db.Where("conversation_id = ? AND user_id = ?", conversationID, userID).First(&participant).Error
if err != nil {
// 如果找不到参与者,尝试添加(修复没有参与者记录的问题)
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 检查会话是否存在
var conv model.Conversation
if err := r.db.Where("id = ?", conversationID).First(&conv).Error; err == nil {
@@ -513,7 +514,7 @@ func (r *MessageRepository) DeleteConversationByGroupID(groupID string) error {
conv, err := r.GetConversationByGroupID(groupID)
if err != nil {
// 如果会话不存在,直接返回
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err

View File

@@ -89,7 +89,7 @@ func (r *OperationLogRepository) DeleteOldLogs(ctx context.Context, beforeDate s
return r.db.WithContext(ctx).Where("created_at < ?", beforeDate).Delete(&model.OperationLog{}).Error
}
// GetStatistics 获取操作日志统计
// GetStatistics 获取操作日志统计(使用单次 GROUP BY 查询,避免多次 COUNT)
func (r *OperationLogRepository) GetStatistics(ctx context.Context, startTime, endTime string) (*OperationLogStatistics, error) {
stats := &OperationLogStatistics{}
@@ -101,16 +101,25 @@ func (r *OperationLogRepository) GetStatistics(ctx context.Context, startTime, e
query = query.Where("created_at <= ?", endTime)
}
if err := query.Count(&stats.TotalCount).Error; err != nil {
// 使用单次 GROUP BY 查询获取各状态计数
type statusCount struct {
Status string
Count int64
}
var results []statusCount
err := query.Select("status, count(*) as count").Group("status").Scan(&results).Error
if err != nil {
return nil, err
}
if err := query.Where("status = ?", "success").Count(&stats.SuccessCount).Error; err != nil {
return nil, err
// 汇总结果
for _, r := range results {
stats.TotalCount += r.Count
if r.Status == "success" {
stats.SuccessCount = r.Count
} else if r.Status == "fail" {
stats.FailCount = r.Count
}
if err := query.Where("status = ?", "fail").Count(&stats.FailCount).Error; err != nil {
return nil, err
}
return stats, nil

View File

@@ -802,30 +802,101 @@ func (r *PostRepository) GetByIDForAdmin(id string) (*model.Post, error) {
return &post, nil
}
// BatchDelete 批量删除帖子
// BatchDelete 批量删除帖子(使用单次事务批量删除,避免 N+1 问题)
func (r *PostRepository) BatchDelete(ids []string) ([]string, error) {
var failedIDs []string
for _, id := range ids {
if err := r.Delete(id); err != nil {
failedIDs = append(failedIDs, id)
}
if len(ids) == 0 {
return nil, nil
}
return failedIDs, nil
err := r.db.Transaction(func(tx *gorm.DB) error {
// 批量删除帖子图片
if err := tx.Where("post_id IN ?", ids).Delete(&model.PostImage{}).Error; err != nil {
return err
}
// 批量删除帖子点赞记录
if err := tx.Where("post_id IN ?", ids).Delete(&model.PostLike{}).Error; err != nil {
return err
}
// 批量删除帖子收藏记录
if err := tx.Where("post_id IN ?", ids).Delete(&model.Favorite{}).Error; err != nil {
return err
}
// 批量删除评论点赞记录子查询获取所有评论ID
if err := tx.Where("comment_id IN (SELECT id FROM comments WHERE post_id IN ?)", ids).Delete(&model.CommentLike{}).Error; err != nil {
return err
}
// 批量删除帖子评论
if err := tx.Where("post_id IN ?", ids).Delete(&model.Comment{}).Error; err != nil {
return err
}
// 批量删除投票选项
if err := tx.Where("post_id IN ?", ids).Delete(&model.VoteOption{}).Error; err != nil {
return err
}
// 批量删除用户投票记录
if err := tx.Where("post_id IN ?", ids).Delete(&model.UserVote{}).Error; err != nil {
return err
}
// 最后批量删除帖子本身(软删除)
return tx.Where("id IN ?", ids).Delete(&model.Post{}).Error
})
if err != nil {
return ids, err
}
return nil, nil
}
// BatchUpdateStatus 批量更新帖子审核状态
// BatchUpdateStatus 批量更新帖子审核状态(使用单条 SQL 批量更新,避免 N+1 问题)
func (r *PostRepository) BatchUpdateStatus(ids []string, status model.PostStatus, reviewedBy string) ([]string, error) {
var failedIDs []string
if len(ids) == 0 {
return nil, nil
}
// 使用单条 SQL 批量更新
result := r.db.Model(&model.Post{}).
Where("id IN ?", ids).
Updates(map[string]any{
"status": status,
"reviewed_at": gorm.Expr("CURRENT_TIMESTAMP"),
"reviewed_by": reviewedBy,
"reject_reason": "",
"updated_at": gorm.Expr("updated_at"),
})
if result.Error != nil {
return ids, result.Error
}
// 返回实际更新的帖子数量
if result.RowsAffected < int64(len(ids)) {
// 部分帖子可能不存在,查询实际更新的 ID
var updatedIDs []string
r.db.Model(&model.Post{}).Where("id IN ?", ids).Pluck("id", &updatedIDs)
// 找出未更新的 ID
updatedMap := make(map[string]bool, len(updatedIDs))
for _, id := range updatedIDs {
updatedMap[id] = true
}
var failedIDs []string
for _, id := range ids {
if err := r.UpdateModerationStatus(id, status, "", reviewedBy); err != nil {
if !updatedMap[id] {
failedIDs = append(failedIDs, id)
}
}
return failedIDs, nil
}
return nil, nil
}
// UpdatePinStatus 更新帖子置顶状态

View File

@@ -2,6 +2,7 @@ package repository
import (
"carrot_bbs/internal/model"
"strings"
"gorm.io/gorm"
)
@@ -88,18 +89,31 @@ func (r *stickerRepository) UpdateSortOrder(id string, sortOrder int) error {
Update("sort_order", sortOrder).Error
}
// BatchUpdateSortOrder 批量更新排序
// BatchUpdateSortOrder 批量更新排序(使用 CASE WHEN 批量更新,避免 N+1 问题)
func (r *stickerRepository) BatchUpdateSortOrder(userID string, orders map[string]int) error {
return r.db.Transaction(func(tx *gorm.DB) error {
for id, sortOrder := range orders {
if err := tx.Model(&model.UserSticker{}).
Where("id = ? AND user_id = ?", id, userID).
Update("sort_order", sortOrder).Error; err != nil {
return err
}
}
if len(orders) == 0 {
return nil
})
}
// 构建 CASE WHEN 批量更新 SQL
var cases []string
var args []any
for id, sortOrder := range orders {
cases = append(cases, "WHEN id = ? THEN ?")
args = append(args, id, sortOrder)
}
ids := make([]string, 0, len(orders))
for id := range orders {
ids = append(ids, id)
}
sql := `UPDATE user_stickers SET sort_order = CASE ` +
strings.Join(cases, " ") + ` END WHERE id IN ? AND user_id = ?`
args = append(args, ids, userID)
return r.db.Exec(sql, args...).Error
}
// CountByUserID 获取用户表情数量

View File

@@ -140,6 +140,36 @@ func (r *UserRepository) IsFollowing(followerID, followingID string) (bool, erro
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).
@@ -243,6 +273,36 @@ func (r *UserRepository) IsBlocked(blockerID, blockedID string) (bool, error) {
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

View File

@@ -17,21 +17,24 @@ func NewVoteRepository(db *gorm.DB) *VoteRepository {
return &VoteRepository{db: db}
}
// CreateOptions 批量创建投票选项
// CreateOptions 批量创建投票选项(使用批量插入,避免 N+1 问题)
func (r *VoteRepository) CreateOptions(postID string, options []string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if len(options) == 0 {
return nil
}
// 构建批量插入的选项切片
voteOptions := make([]model.VoteOption, len(options))
for i, content := range options {
option := &model.VoteOption{
voteOptions[i] = model.VoteOption{
PostID: postID,
Content: content,
SortOrder: i,
}
if err := tx.Create(option).Error; err != nil {
return err
}
}
return nil
})
// 使用 GORM 的 Create 方法批量插入
return r.db.CreateInBatches(voteOptions, 100).Error
}
// GetOptionsByPostID 获取帖子的所有投票选项
@@ -58,7 +61,7 @@ func (r *VoteRepository) Vote(postID, userID, optionID string) error {
// 验证选项是否属于该帖子
var option model.VoteOption
if err := tx.Where("id = ? AND post_id = ?", optionID, postID).First(&option).Error; err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("invalid option")
}
return err
@@ -86,7 +89,7 @@ func (r *VoteRepository) Unvote(postID, userID string) error {
var userVote model.UserVote
err := tx.Where("post_id = ? AND user_id = ?", postID, userID).First(&userVote).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil // 没有投票记录,直接返回
}
return err
@@ -113,7 +116,7 @@ func (r *VoteRepository) GetUserVote(postID, userID string) (*model.UserVote, er
var userVote model.UserVote
err := r.db.Where("post_id = ? AND user_id = ?", postID, userID).First(&userVote).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err