optimize the list of comments

This commit is contained in:
landaiqing
2024-09-25 11:46:49 +08:00
parent bb2f49fe74
commit 97175c3d67
9 changed files with 333 additions and 99 deletions

View File

@@ -14,6 +14,7 @@ import (
type CommentAPI struct{}
var wg sync.WaitGroup
var mx sync.Mutex
var commentReplyService = service.Service.CommentReplyService
// CommentImages 评论图片
@@ -42,10 +43,10 @@ type CommentContent struct {
Likes int64 `json:"likes"`
ReplyCount int64 `json:"reply_count"`
CreatedTime time.Time `json:"created_time"`
Dislikes int64 `json:"dislikes"`
Location string `json:"location"`
Browser string `json:"browser"`
OperatingSystem string `json:"operating_system"`
IsLiked bool `json:"is_liked" default:"false"`
Images []string `json:"images,omitempty"`
}

View File

@@ -3,7 +3,6 @@ package comment_api
import (
"context"
"encoding/base64"
"errors"
"fmt"
"github.com/acmestack/gorm-plus/gplus"
ginI18n "github.com/gin-contrib/i18n"
@@ -392,63 +391,126 @@ func (CommentAPI) CommentList(c *gin.Context) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
return
}
// 查询评论列表
query, u := gplus.NewQuery[model.ScaCommentReply]()
page := gplus.NewPage[model.ScaCommentReply](commentListRequest.Page, commentListRequest.Size)
query.Eq(&u.TopicId, commentListRequest.TopicId).Eq(&u.CommentType, enum.COMMENT).OrderByDesc(&u.Likes)
page, _ = gplus.SelectPage(page, query)
query.Eq(&u.TopicId, commentListRequest.TopicId).Eq(&u.CommentType, enum.COMMENT).OrderByDesc(&u.CommentOrder).OrderByDesc(&u.Likes).OrderByDesc(&u.ReplyCount).OrderByDesc(&u.CreatedTime)
page, pageDB := gplus.SelectPage(page, query)
if pageDB.Error != nil {
global.LOG.Errorln(pageDB.Error)
return
}
if len(page.Records) == 0 {
result.OkWithData(CommentResponse{Comments: []CommentContent{}, Size: page.Size, Current: page.Current, Total: page.Total}, c)
return
}
userIds := make([]string, 0, len(page.Records))
commentIds := make([]int64, 0, len(page.Records))
for _, comment := range page.Records {
userIds = append(userIds, comment.UserId)
commentIds = append(commentIds, comment.Id)
}
queryUser, n := gplus.NewQuery[model.ScaAuthUser]()
queryUser.In(&n.UID, userIds)
userInfos, _ := gplus.SelectList(queryUser)
// 结果存储
userInfoMap := make(map[string]model.ScaAuthUser)
likeMap := make(map[int64]bool)
commentImagesMap := make(map[int64]CommentImages)
userInfoMap := make(map[string]model.ScaAuthUser, len(userInfos))
for _, userInfo := range userInfos {
userInfoMap[*userInfo.UID] = *userInfo
}
// 使用 WaitGroup 等待协程完成
wg.Add(3)
commentChannel := make(chan CommentContent, len(page.Records))
imagesBase64S := make([][]string, len(page.Records)) // 存储每条评论的图片
// 查询评论用户信息
go func() {
defer wg.Done()
queryUser, n := gplus.NewQuery[model.ScaAuthUser]()
queryUser.Select(&n.UID, &n.Avatar, &n.Nickname).In(&n.UID, userIds)
userInfos, userInfosDB := gplus.SelectList(queryUser)
if userInfosDB.Error != nil {
global.LOG.Errorln(userInfosDB.Error)
return
}
for _, userInfo := range userInfos {
userInfoMap[*userInfo.UID] = *userInfo
}
}()
for index, comment := range page.Records {
wg.Add(1)
go func(comment model.ScaCommentReply, index int) {
defer wg.Done()
// 使用 context 设置超时时间
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // 设置超时2秒
defer cancel()
// 获取评论图片并处理
commentImages := CommentImages{}
wrong := global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").FindOne(ctx, bson.M{"comment_id": comment.Id}).Decode(&commentImages)
if wrong != nil && !errors.Is(wrong, mongo.ErrNoDocuments) {
global.LOG.Errorf("Failed to get images for comment ID %s: %v", comment.Id, wrong)
// 查询评论点赞状态
go func() {
defer wg.Done()
if len(page.Records) > 0 {
queryLike, l := gplus.NewQuery[model.ScaCommentLikes]()
queryLike.Eq(&l.TopicId, commentListRequest.TopicId).Eq(&l.UserId, commentListRequest.UserID).In(&l.CommentId, commentIds)
likes, likesDB := gplus.SelectList(queryLike)
if likesDB.Error != nil {
global.LOG.Errorln(likesDB.Error)
return
}
for _, like := range likes {
likeMap[like.CommentId] = true
}
}
}()
// 查询评论图片信息
go func() {
defer wg.Done()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // 设置超时2秒
defer cancel()
cursor, err := global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").Find(ctx, bson.M{"comment_id": bson.M{"$in": commentIds}})
if err != nil {
global.LOG.Errorf("Failed to get images for comments: %v", err)
return
}
defer func(cursor *mongo.Cursor, ctx context.Context) {
err := cursor.Close(ctx)
if err != nil {
return
}
}(cursor, ctx)
for cursor.Next(ctx) {
var commentImages CommentImages
if err = cursor.Decode(&commentImages); err != nil {
global.LOG.Errorf("Failed to decode comment images: %v", err)
continue
}
commentImagesMap[commentImages.CommentId] = commentImages
}
}()
// 等待所有查询完成
wg.Wait()
commentChannel := make(chan CommentContent, len(page.Records))
for _, comment := range page.Records {
wg.Add(1)
go func(comment model.ScaCommentReply) {
defer wg.Done()
// 将图片转换为base64
var imagesBase64 []string
for _, img := range commentImages.Images {
mimeType := getMimeType(img)
base64Img := base64.StdEncoding.EncodeToString(img)
base64WithPrefix := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img)
imagesBase64 = append(imagesBase64, base64WithPrefix)
if imgData, ok := commentImagesMap[comment.Id]; ok {
// 将图片转换为base64
for _, img := range imgData.Images {
mimeType := getMimeType(img)
base64Img := base64.StdEncoding.EncodeToString(img)
base64WithPrefix := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img)
imagesBase64 = append(imagesBase64, base64WithPrefix)
}
}
imagesBase64S[index] = imagesBase64 // 保存到切片中
userInfo := userInfoMap[comment.UserId]
userInfo, exist := userInfoMap[comment.UserId]
if !exist {
global.LOG.Errorf("Failed to get user info for comment: %s", comment.UserId)
return
}
commentContent := CommentContent{
Avatar: *userInfo.Avatar,
NickName: *userInfo.Nickname,
Id: comment.Id,
UserId: comment.UserId,
TopicId: comment.TopicId,
Dislikes: comment.Dislikes,
Content: comment.Content,
ReplyCount: comment.ReplyCount,
Likes: comment.Likes,
@@ -458,9 +520,10 @@ func (CommentAPI) CommentList(c *gin.Context) {
Browser: comment.Browser,
OperatingSystem: comment.OperatingSystem,
Images: imagesBase64,
IsLiked: likeMap[comment.Id],
}
commentChannel <- commentContent
}(*comment, index)
}(*comment)
}
go func() {
@@ -500,81 +563,131 @@ func (CommentAPI) ReplyList(c *gin.Context) {
}
query, u := gplus.NewQuery[model.ScaCommentReply]()
page := gplus.NewPage[model.ScaCommentReply](replyListRequest.Page, replyListRequest.Size)
query.Eq(&u.TopicId, replyListRequest.TopicId).Eq(&u.ReplyId, replyListRequest.CommentId).Eq(&u.CommentType, enum.REPLY).OrderByDesc(&u.Likes)
page, _ = gplus.SelectPage(page, query)
query.Eq(&u.TopicId, replyListRequest.TopicId).Eq(&u.ReplyId, replyListRequest.CommentId).Eq(&u.CommentType, enum.REPLY).OrderByDesc(&u.CommentOrder).OrderByDesc(&u.Likes).OrderByDesc(&u.CreatedTime)
page, pageDB := gplus.SelectPage(page, query)
if pageDB.Error != nil {
global.LOG.Errorln(pageDB.Error)
return
}
if len(page.Records) == 0 {
result.OkWithData(CommentResponse{Comments: []CommentContent{}, Size: page.Size, Current: page.Current, Total: page.Total}, c)
return
}
userIds := make([]string, 0, len(page.Records))
replyUserIds := make([]string, 0, len(page.Records))
// 收集用户 ID 和回复用户 ID
userIdsSet := make(map[string]struct{}) // 使用集合去重用户 ID
commentIds := make([]int64, 0, len(page.Records))
// 收集用户 ID 和评论 ID
for _, comment := range page.Records {
userIds = append(userIds, comment.UserId)
userIdsSet[comment.UserId] = struct{}{} // 去重
commentIds = append(commentIds, comment.Id)
if comment.ReplyUser != "" {
replyUserIds = append(replyUserIds, comment.ReplyUser)
userIdsSet[comment.ReplyUser] = struct{}{} // 去重
}
}
// 查询评论用户信息
queryUser, n := gplus.NewQuery[model.ScaAuthUser]()
queryUser.In(&n.UID, userIds)
userInfos, _ := gplus.SelectList(queryUser)
userInfoMap := make(map[string]model.ScaAuthUser, len(userInfos))
for _, userInfo := range userInfos {
userInfoMap[*userInfo.UID] = *userInfo
// 将用户 ID 转换为切片
userIds := make([]string, 0, len(userIdsSet))
for userId := range userIdsSet {
userIds = append(userIds, userId)
}
// 查询回复用户信息
replyUserInfoMap := make(map[string]model.ScaAuthUser)
if len(replyUserIds) > 0 {
queryReplyUser, m := gplus.NewQuery[model.ScaAuthUser]()
queryReplyUser.In(&m.UID, replyUserIds)
replyUserInfos, _ := gplus.SelectList(queryReplyUser)
likeMap := make(map[int64]bool, len(page.Records))
commentImagesMap := make(map[int64]CommentImages)
userInfoMap := make(map[string]model.ScaAuthUser, len(userIds))
for _, replyUserInfo := range replyUserInfos {
replyUserInfoMap[*replyUserInfo.UID] = *replyUserInfo
wg.Add(3)
go func() {
defer wg.Done()
// 查询评论用户信息
queryUser, n := gplus.NewQuery[model.ScaAuthUser]()
queryUser.Select(&n.UID, &n.Avatar, &n.Nickname).In(&n.UID, userIds)
userInfos, userInfosDB := gplus.SelectList(queryUser)
if userInfosDB.Error != nil {
global.LOG.Errorln(userInfosDB.Error)
return
}
}
for _, userInfo := range userInfos {
userInfoMap[*userInfo.UID] = *userInfo
}
}()
replyChannel := make(chan CommentContent, len(page.Records)) // 使用通道传递回复内容
imagesBase64S := make([][]string, len(page.Records)) // 存储每条回复的图片
go func() {
defer wg.Done()
// 查询评论点赞状态
for index, reply := range page.Records {
wg.Add(1)
go func(reply model.ScaCommentReply, index int) {
defer wg.Done()
// 使用 context 设置超时时间
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // 设置超时2秒
defer cancel()
// 获取回复图片并处理
replyImages := CommentImages{}
wrong := global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").FindOne(ctx, bson.M{"comment_id": reply.Id}).Decode(&replyImages)
if wrong != nil && !errors.Is(wrong, mongo.ErrNoDocuments) {
global.LOG.Errorf("Failed to get images for reply ID %s: %v", reply.Id, wrong)
if len(page.Records) > 0 {
queryLike, l := gplus.NewQuery[model.ScaCommentLikes]()
queryLike.Eq(&l.TopicId, replyListRequest.TopicId).Eq(&l.UserId, replyListRequest.UserID).In(&l.CommentId, commentIds)
likes, likesDB := gplus.SelectList(queryLike)
if likesDB.Error != nil {
global.LOG.Errorln(likesDB.Error)
return
}
// 将图片转换为base64
var imagesBase64 []string
for _, img := range replyImages.Images {
mimeType := getMimeType(img)
base64Img := base64.StdEncoding.EncodeToString(img)
base64WithPrefix := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img)
imagesBase64 = append(imagesBase64, base64WithPrefix)
for _, like := range likes {
likeMap[like.CommentId] = true
}
imagesBase64S[index] = imagesBase64 // 保存到切片中
}
}()
userInfo := userInfoMap[reply.UserId]
replyUserInfo := replyUserInfoMap[reply.ReplyUser]
go func() {
defer wg.Done()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // 设置超时2秒
defer cancel()
cursor, err := global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").Find(ctx, bson.M{"comment_id": bson.M{"$in": commentIds}})
if err != nil {
global.LOG.Errorf("Failed to get images for comments: %v", err)
return
}
defer func(cursor *mongo.Cursor, ctx context.Context) {
warn := cursor.Close(ctx)
if warn != nil {
return
}
}(cursor, ctx)
for cursor.Next(ctx) {
var commentImages CommentImages
if e := cursor.Decode(&commentImages); e != nil {
global.LOG.Errorf("Failed to decode comment images: %v", e)
continue
}
commentImagesMap[commentImages.CommentId] = commentImages
}
}()
wg.Wait()
replyChannel := make(chan CommentContent, len(page.Records)) // 使用通道传递回复内容
for _, reply := range page.Records {
wg.Add(1)
go func(reply model.ScaCommentReply) {
defer wg.Done()
var imagesBase64 []string
if imgData, ok := commentImagesMap[reply.Id]; ok {
// 将图片转换为base64
for _, img := range imgData.Images {
mimeType := getMimeType(img)
base64Img := base64.StdEncoding.EncodeToString(img)
base64WithPrefix := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img)
imagesBase64 = append(imagesBase64, base64WithPrefix)
}
}
userInfo, exist := userInfoMap[reply.UserId]
if !exist {
global.LOG.Errorf("Failed to get user info for comment: %s", reply.UserId)
return
}
replyUserInfo, e := userInfoMap[reply.ReplyUser]
if !e {
global.LOG.Errorf("Failed to get reply user info for comment: %s", reply.ReplyUser)
return
}
commentContent := CommentContent{
Avatar: *userInfo.Avatar,
NickName: *userInfo.Nickname,
Id: reply.Id,
UserId: reply.UserId,
TopicId: reply.TopicId,
Dislikes: reply.Dislikes,
Content: reply.Content,
ReplyUsername: *replyUserInfo.Nickname,
ReplyCount: reply.ReplyCount,
@@ -588,9 +701,10 @@ func (CommentAPI) ReplyList(c *gin.Context) {
Browser: reply.Browser,
OperatingSystem: reply.OperatingSystem,
Images: imagesBase64,
IsLiked: likeMap[reply.Id],
}
replyChannel <- commentContent // 发送到通道
}(*reply, index)
}(*reply)
}
go func() {
@@ -614,6 +728,13 @@ func (CommentAPI) ReplyList(c *gin.Context) {
}
// CommentLikes 点赞评论
// @Summary 点赞评论
// @Description 点赞评论
// @Tags 评论
// @Accept json
// @Produce json
// @Param comment_like_request body dto.CommentLikeRequest true "点赞请求"
// @Router /auth/comment/like [post]
func (CommentAPI) CommentLikes(c *gin.Context) {
likeRequest := dto.CommentLikeRequest{}
err := c.ShouldBindJSON(&likeRequest)
@@ -621,8 +742,79 @@ func (CommentAPI) CommentLikes(c *gin.Context) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
return
}
mx.Lock()
defer mx.Unlock()
likes := model.ScaCommentLikes{
CommentId: likeRequest.CommentId,
UserId: likeRequest.UserID,
TopicId: likeRequest.TopicId,
}
tx := global.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
res := gplus.Insert(&likes)
if res.Error != nil {
tx.Rollback()
global.LOG.Errorln(res.Error)
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentLikeFailed"), c)
return
}
// 更新点赞计数
if err = commentReplyService.UpdateCommentLikesCount(likeRequest.CommentId, likeRequest.TopicId); err != nil {
tx.Rollback()
global.LOG.Errorln(err)
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentLikeFailed"), c)
return
}
tx.Commit()
result.OkWithMessage(ginI18n.MustGetMessage(c, "CommentLikeSuccess"), c)
return
}
func (CommentAPI) CommentDislikes(c *gin.Context) {
// CancelCommentLikes 取消点赞评论
// @Summary 取消点赞评论
// @Description 取消点赞评论
// @Tags 评论
// @Accept json
// @Produce json
// @Param comment_like_request body dto.CommentLikeRequest true "取消点赞请求"
// @Router /auth/comment/cancel_like [post]
func (CommentAPI) CancelCommentLikes(c *gin.Context) {
likeRequest := dto.CommentLikeRequest{}
err := c.ShouldBindJSON(&likeRequest)
if err != nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
return
}
mx.Lock()
defer mx.Unlock()
tx := global.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
query, u := gplus.NewQuery[model.ScaCommentLikes]()
query.Eq(&u.CommentId, likeRequest.CommentId).Eq(&u.UserId, likeRequest.UserID).Eq(&u.TopicId, likeRequest.TopicId)
res := gplus.Delete[model.ScaCommentLikes](query)
if res.Error != nil {
tx.Rollback()
global.LOG.Errorln(res.Error)
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentLikeCancelFailed"), c)
return
}
err = commentReplyService.DecrementCommentLikesCount(likeRequest.CommentId, likeRequest.TopicId)
if err != nil {
tx.Rollback()
global.LOG.Errorln(err)
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentLikeCancelFailed"), c)
return
}
tx.Commit()
result.OkWithMessage(ginI18n.MustGetMessage(c, "CommentLikeCancelSuccess"), c)
return
}

View File

@@ -25,15 +25,17 @@ type ReplyReplyRequest struct {
ReplyTo int64 `json:"reply_to" binding:"required"`
ReplyId int64 `json:"reply_id" binding:"required"`
ReplyUser string `json:"reply_user" binding:"required"`
Author string `json:"author" binding:"required""`
Author string `json:"author" binding:"required"`
}
type CommentListRequest struct {
UserID string `json:"user_id" binding:"required"`
TopicId string `json:"topic_id" binding:"required"`
Page int `json:"page" default:"1"`
Size int `json:"size" default:"5"`
}
type ReplyListRequest struct {
UserID string `json:"user_id" binding:"required"`
TopicId string `json:"topic_id" binding:"required"`
CommentId int64 `json:"comment_id" binding:"required"`
Page int `json:"page" default:"1"`
@@ -43,5 +45,4 @@ type CommentLikeRequest struct {
TopicId string `json:"topic_id" binding:"required"`
CommentId int64 `json:"comment_id" binding:"required"`
UserID string `json:"user_id" binding:"required"`
Type int `json:"type" binding:"required"`
}

View File

@@ -69,4 +69,8 @@ CommentSubmitFailed = "comment submit failed!"
CommentSubmitSuccess = "comment submit successfully!"
ImageStorageError = "image storage error!"
ImageDecodeError = "image decode error!"
ImageSaveError = "image save error!"
ImageSaveError = "image save error!"
CommentLikeSuccess = "comment like success!"
CommentLikeFailed = "comment like failed!"
CommentDislikeSuccess = "comment dislike success!"
CommentDislikeFailed = "comment dislike failed!"

View File

@@ -69,4 +69,8 @@ CommentSubmitFailed = "评论提交失败!"
CommentSubmitSuccess = "评论提交成功!"
ImageStorageError = "图片存储错误!"
ImageDecodeError = "图片解码错误!"
ImageSaveError = "图片保存错误!"
ImageSaveError = "图片保存错误!"
CommentLikeSuccess = "评论点赞成功!"
CommentLikeFailed = "评论点赞失败!"
CommentDislikeSuccess = "评论取消点赞成功!"
CommentDislikeFailed = "评论取消点赞失败!"

View File

@@ -8,7 +8,6 @@ type ScaCommentLikes struct {
TopicId string `gorm:"column:topic_id;type:varchar(20);NOT NULL;comment:主题ID" json:"topic_id"`
UserId string `gorm:"column:user_id;type:varchar(20);comment:用户ID;NOT NULL" json:"user_id"`
CommentId int64 `gorm:"column:comment_id;type:bigint(20);comment:评论ID;NOT NULL" json:"comment_id"`
Type int `gorm:"column:type;type:int(11);comment:类型0 点赞 1 踩;NOT NULL" json:"type"`
}
func (like *ScaCommentLikes) TableName() string {

View File

@@ -26,7 +26,6 @@ type ScaCommentReply struct {
Deleted int `gorm:"column:deleted;type:int(11);default:0;comment:是否删除 0未删除 1 已删除" json:"-"`
CreatedBy string `gorm:"column:created_by;type:varchar(32);comment:创建人" json:"-"`
UpdateBy string `gorm:"column:update_by;type:varchar(32);comment:更新人" json:"-"`
Dislikes int64 `gorm:"column:dislikes;type:bigint(20);default:0;comment:踩数" json:"dislikes"`
CommentIp string `gorm:"column:comment_ip;type:varchar(20);comment:评论ip" json:"-"`
Location string `gorm:"column:location;type:varchar(20);comment:评论地址" json:"location"`
Browser string `gorm:"column:browser;type:varchar(20);comment:评论浏览器" json:"browser"`

View File

@@ -13,4 +13,6 @@ func CommentRouter(router *gin.RouterGroup) {
router.POST("/auth/comment/list", commonApi.CommentList)
router.POST("/auth/reply/list", commonApi.ReplyList)
router.POST("/auth/reply/reply/submit", commonApi.ReplyReplySubmit)
router.POST("/auth/comment/like", commonApi.CommentLikes)
router.POST("/auth/comment/cancel_like", commonApi.CancelCommentLikes)
}

View File

@@ -56,3 +56,35 @@ func (CommentReplyService) UpdateCommentReplyCount(commentID int64) error {
})
return err
}
// UpdateCommentLikesCount 更新评论 likes 数量
func (CommentReplyService) UpdateCommentLikesCount(commentID int64, topicID string) error {
// 使用事务处理错误
err := global.DB.Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.ScaCommentReply{}).Where("id = ? and topic_id = ? and deleted = 0", commentID, topicID).Update("likes", gorm.Expr("likes + ?", 1))
if result.Error != nil {
return result.Error // 返回更新错误
}
if result.RowsAffected == 0 {
return fmt.Errorf("comment not found") // 处理评论不存在的情况
}
return nil
})
return err
}
// DecrementCommentLikesCount 减少评论 likes 数量
func (CommentReplyService) DecrementCommentLikesCount(commentID int64, topicID string) error {
// 使用事务处理错误
err := global.DB.Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.ScaCommentReply{}).Where("id = ? and topic_id = ? and deleted = 0", commentID, topicID).Update("likes", gorm.Expr("likes - ?", 1))
if result.Error != nil {
return result.Error // 返回更新错误
}
if result.RowsAffected == 0 {
return fmt.Errorf("comment not found") // 处理评论不存在的情况
}
return nil
})
return err
}